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",