diff --git a/apps/desktop/resources/windows/crossdesk_session_helper.manifest b/apps/desktop/resources/windows/crossdesk_session_helper.manifest new file mode 100644 index 00000000..ffd4aa19 --- /dev/null +++ b/apps/desktop/resources/windows/crossdesk_session_helper.manifest @@ -0,0 +1,23 @@ + + + + CrossDesk Session Helper + + + + + + + + + + true/pm + PerMonitorV2 + + + + + + + + diff --git a/apps/desktop/resources/windows/crossdesk_session_helper.rc b/apps/desktop/resources/windows/crossdesk_session_helper.rc index f9a8412e..16d2e7fe 100644 --- a/apps/desktop/resources/windows/crossdesk_session_helper.rc +++ b/apps/desktop/resources/windows/crossdesk_session_helper.rc @@ -4,13 +4,10 @@ IDI_ICON1 ICON "crossdesk.ico" #define CREATEPROCESS_MANIFEST_RESOURCE_ID 1 #define RT_MANIFEST 24 -#ifdef CROSSDESK_PORTABLE -CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest" -#elif defined(CROSSDESK_DEBUG) -CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest" -#else -CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest" -#endif +// The service supplies either the user's token (desktop observer) or a SYSTEM +// token (secure capture/input). Requiring elevation here prevents the observer +// from starting with CreateProcessAsUserW in installed release builds (740). +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_session_helper.manifest" #define CROSSDESK_FILE_DESCRIPTION "CrossDesk Session Helper" #define CROSSDESK_ORIGINAL_FILENAME "crossdesk_session_helper.exe" diff --git a/apps/desktop/src/app/main.cpp b/apps/desktop/src/app/main.cpp index 9c70bf50..f4365a7c 100644 --- a/apps/desktop/src/app/main.cpp +++ b/apps/desktop/src/app/main.cpp @@ -20,6 +20,7 @@ #include "platform/windows/gui/slint_backend.h" #include "service_host.h" #include "service_lifetime.h" +#include "shutdown_order.h" #include "unattended_config.h" #endif @@ -406,6 +407,16 @@ static int RunApplication( } int main(int argc, char* argv[]) { +#ifdef _WIN32 + // Apply in every process, including the daemon and --child. Windows does + // not inherit shutdown priority from a parent process. + const DWORD shutdown_error = crossdesk::ConfigureWindowsShutdownOrder( + crossdesk::WindowsShutdownRole::host); + if (shutdown_error != ERROR_SUCCESS) { + std::cerr << "Could not configure Windows host shutdown order, error=" + << shutdown_error << '\n'; + } +#endif #ifdef __linux__ // Run before GUI, configuration, logging or display initialization. if (argc == 2 && diff --git a/apps/desktop/src/gui/application/gui_application.cpp b/apps/desktop/src/gui/application/gui_application.cpp index a6eeb50f..ab962077 100644 --- a/apps/desktop/src/gui/application/gui_application.cpp +++ b/apps/desktop/src/gui/application/gui_application.cpp @@ -56,6 +56,7 @@ #include #include "platform/windows/gui/slint_backend.h" +#include "platform/windows/gui/shutdown_session_guard.h" #include "platform/windows/gui/tray/win_tray.h" #include "platform/windows/gui/window_icons.h" #elif defined(__APPLE__) @@ -923,6 +924,10 @@ void DragWindow(WindowHandle& component, int phase, float mouse_x, } // namespace struct GuiApplication::SlintUi { +#if _WIN32 + std::unique_ptr shutdown_guard; + std::wstring_view shutdown_reason; +#endif gallery::Workspace gallery; std::filesystem::path gallery_path; std::vector gallery_ids; @@ -1284,6 +1289,10 @@ void GuiApplication::InitializeUi() { slint::set_xdg_app_id("crossdesk"); #endif ui_ = std::make_unique(); +#if _WIN32 + ui_->shutdown_guard = + std::make_unique(windows_rdp_handoff_connected_); +#endif ui_->sponsorship_prompt_pending = config_center_->GetSponsorshipPromptDismissedVersion() != CROSSDESK_VERSION; ui_->video_presenter = @@ -2258,6 +2267,11 @@ void GuiApplication::Tick() { slint::quit_event_loop(); return; } +#if _WIN32 + if (ui_->shutdown_guard) { + ui_->shutdown_guard->Update(ui_->shutdown_reason); + } +#endif PollUpdateCheck(); HandleSessionCleanup(); HandlePasswordChangeResult(); @@ -2815,6 +2829,15 @@ void GuiApplication::UpdateLocalization() { if (!ui_ || ui_->localized_language == language) { return; } +#if _WIN32 + // All views refer to static literals; selecting the text needs no allocation + // and only runs when the language changes. Tick still checks connection state. + ui_->shutdown_reason = language == 0 + ? L"远程控制连接中,保留连接以确认或取消关机。" + : language == 2 + ? L"Удалённый сеанс активен. Соединение сохранено для подтверждения или отмены выключения." + : L"Remote session active. Keeping the connection available to confirm or cancel shutdown."; +#endif ui_localization::ApplyMainWindowStrings(ui_->main, language); ui_localization::ApplyStreamWindowStrings(ui_->main, language); ui_localization::ApplyMainWindowStrings(ui_->wall, language); @@ -6191,6 +6214,9 @@ 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/platform/windows/gui/shutdown_session_guard.cpp b/apps/desktop/src/platform/windows/gui/shutdown_session_guard.cpp new file mode 100644 index 00000000..ce3715b8 --- /dev/null +++ b/apps/desktop/src/platform/windows/gui/shutdown_session_guard.cpp @@ -0,0 +1,121 @@ +#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 new file mode 100644 index 00000000..6cfdb91b --- /dev/null +++ b/apps/desktop/src/platform/windows/gui/shutdown_session_guard.h @@ -0,0 +1,48 @@ +/* + * @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/service/interactive_desktop.h b/apps/desktop/src/platform/windows/service/interactive_desktop.h index 63339ccc..ac78fd23 100644 --- a/apps/desktop/src/platform/windows/service/interactive_desktop.h +++ b/apps/desktop/src/platform/windows/service/interactive_desktop.h @@ -1,6 +1,6 @@ /* * @Author: DI JUNKUN - * @Date: 2026-09-07 + * @Date: 2026-10-22 * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */ @@ -19,6 +19,15 @@ inline bool IsDesktopTransitionInputError(DWORD error) { error == ERROR_REQUIRES_INTERACTIVE_WINDOWSTATION; } +// Only call with an observation from the interactive session, never Session 0. +// Process presence (including LogonUI) cannot prove that Default is accessible: +// shutdown blocking UI may be active while WTS still reports an unlocked user. +inline bool IsSecureInputDesktop(bool available, const std::string& name, + DWORD error) { + if (!available) return error == ERROR_ACCESS_DENIED; + return !name.empty() && _stricmp(name.c_str(), "Default") != 0; +} + inline std::wstring GetDesktopNameW(HDESK desktop) { DWORD bytes_needed = 0; if (desktop == nullptr) { diff --git a/apps/desktop/src/platform/windows/service/session_helper_main.cpp b/apps/desktop/src/platform/windows/service/session_helper_main.cpp index f577cd1c..f3911b74 100644 --- a/apps/desktop/src/platform/windows/service/session_helper_main.cpp +++ b/apps/desktop/src/platform/windows/service/session_helper_main.cpp @@ -32,6 +32,7 @@ #include "secure_desktop_frame_pacer.h" #include "secure_desktop_nv12_cache.h" #include "service_lifetime.h" +#include "shutdown_order.h" #include "shared_capture_lifecycle.h" #include "shared_capture_configuration.h" #include "usbmmidd_policy.h" @@ -398,13 +399,8 @@ void UpdateHelperState(HelperState* helper_state) { // Process enumeration can straddle an unlock. Observe the input desktop // afterwards, and expose the sample's start time for service-side rejection. InputDesktopInfo desktop_info = GetInputDesktopInfo(); - const bool input_desktop_is_winlogon = - _stricmp(desktop_info.name.c_str(), "Winlogon") == 0; - const bool inaccessible_secure_input_desktop = - !desktop_info.available && - desktop_info.error_code == ERROR_ACCESS_DENIED && !logon_ui_visible; - const bool secure_desktop_active = input_desktop_is_winlogon || - inaccessible_secure_input_desktop; + const bool secure_desktop_active = crossdesk::IsSecureInputDesktop( + desktop_info.available, desktop_info.name, desktop_info.error_code); bool session_locked = false; if (!QuerySessionLockState(helper_state->session_id, &session_locked)) { session_locked = @@ -2043,9 +2039,15 @@ void PrintUsage() { } // namespace int main(int argc, char* argv[]) { + const DWORD shutdown_error = crossdesk::ConfigureWindowsShutdownOrder( + crossdesk::WindowsShutdownRole::helper); EnablePerMonitorDpiAwareness(); InitializeHelperLogger(); + if (shutdown_error != ERROR_SUCCESS) { + LOG_WARN("Could not configure Windows helper shutdown order, error={}", + shutdown_error); + } bool run_helper = false; bool run_secure_input_helper = false; @@ -2158,6 +2160,8 @@ int main(int argc, char* argv[]) { bool last_consent_ui = false; bool last_secure_desktop = false; bool last_session_locked = false; + bool last_input_desktop_available = false; + DWORD last_input_desktop_error = ERROR_SUCCESS; std::string last_stage; while (true) { UpdateHelperState(&helper_state); @@ -2165,6 +2169,7 @@ int main(int argc, char* argv[]) { std::string desktop_name; bool session_locked = false; bool input_desktop_available = false; + DWORD input_desktop_error = ERROR_SUCCESS; bool lock_app_visible = false; bool logon_ui_running = false; bool consent_ui_visible = false; @@ -2174,6 +2179,7 @@ int main(int argc, char* argv[]) { desktop_name = helper_state.input_desktop_name; session_locked = helper_state.session_locked; input_desktop_available = helper_state.input_desktop_available; + input_desktop_error = helper_state.input_desktop_error_code; lock_app_visible = helper_state.lock_app_visible; logon_ui_running = helper_state.logon_ui_visible; consent_ui_visible = helper_state.consent_ui_visible; @@ -2188,6 +2194,8 @@ int main(int argc, char* argv[]) { lock_app_visible, credential_ui_visible, secure_desktop_active); if (desktop_name != last_desktop_name || + input_desktop_available != last_input_desktop_available || + input_desktop_error != last_input_desktop_error || session_locked != last_session_locked || lock_app_visible != last_lock_app || logon_ui_running != last_logon_ui || @@ -2195,11 +2203,15 @@ int main(int argc, char* argv[]) { secure_desktop_active != last_secure_desktop || stage != last_stage) { LOG_INFO( "Session helper state: session_id={}, input_desktop='{}', " + "input_desktop_available={}, input_desktop_error={}, " "session_locked={}, lock_app_visible={}, logon_ui_running={}, " "consent_ui_visible={}, secure_desktop_active={}, stage={}", - current_session_id, desktop_name, session_locked, lock_app_visible, + current_session_id, desktop_name, input_desktop_available, + input_desktop_error, session_locked, lock_app_visible, logon_ui_running, consent_ui_visible, secure_desktop_active, stage); last_desktop_name = desktop_name; + last_input_desktop_available = input_desktop_available; + last_input_desktop_error = input_desktop_error; last_session_locked = session_locked; last_lock_app = lock_app_visible; last_logon_ui = logon_ui_running; diff --git a/apps/desktop/src/platform/windows/service/shutdown_order.h b/apps/desktop/src/platform/windows/service/shutdown_order.h new file mode 100644 index 00000000..f13cfddc --- /dev/null +++ b/apps/desktop/src/platform/windows/service/shutdown_order.h @@ -0,0 +1,35 @@ +/* + * @Author: DI JUNKUN + * @Date: 2026-10-22 + * Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. + */ + +#ifndef _SHUTDOWN_ORDER_H_ +#define _SHUTDOWN_ORDER_H_ + +#include + +namespace crossdesk { + +enum class WindowsShutdownRole { host, helper }; + +// Windows shuts processes down from high to low (the default is 0x280). +// Keep the remote host alive while ordinary applications ask the user to +// resolve a blocked shutdown. The helpers must outlive the host. Stay inside +// the application range; 0x000-0x0ff is reserved for the operating system. +inline constexpr DWORD WindowsShutdownLevel(WindowsShutdownRole role) { + return role == WindowsShutdownRole::host ? 0x101 : 0x100; +} + +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. + if (SetProcessShutdownParameters(WindowsShutdownLevel(role), 0)) + return ERROR_SUCCESS; + const DWORD error = GetLastError(); + return error != ERROR_SUCCESS ? error : ERROR_GEN_FAILURE; +} + +} // namespace crossdesk + +#endif diff --git a/apps/desktop/xmake/targets.lua b/apps/desktop/xmake/targets.lua index b99a8940..86e5298a 100644 --- a/apps/desktop/xmake/targets.lua +++ b/apps/desktop/xmake/targets.lua @@ -20,10 +20,6 @@ function setup_targets() end table.insert(windows_resource_defines, "CROSSDESK_VERSION_NUMERIC=" .. table.concat(version_parts, ",")) - local windows_helper_resource_defines = table.join(windows_resource_defines) - if is_config("CROSSDESK_PORTABLE", true) then - table.insert(windows_helper_resource_defines, "CROSSDESK_PORTABLE=1") - end local function copy_slint_runtime(target) if not target:is_plat("windows") then @@ -607,6 +603,7 @@ function setup_targets() add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp", "apps/desktop/src/platform/windows/gui/slint_backend.cpp", "apps/desktop/src/platform/windows/gui/window_icons.cpp", + "apps/desktop/src/platform/windows/gui/shutdown_session_guard.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", @@ -681,7 +678,7 @@ function setup_targets() add_files("apps/desktop/src/platform/windows/service/session_helper_main.cpp") add_files("apps/desktop/src/platform/windows/virtual_display/usbmmidd_virtual_display.cpp") add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc", - {defines = windows_helper_resource_defines}) + {defines = windows_resource_defines}) add_includedirs("apps/desktop/src/common", "apps/desktop/src/platform/windows/service", "apps/desktop/src/platform/windows/screen_capturer",