Compare commits

..
12 changed files with 303 additions and 25 deletions
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity version="1.0.0.0" name="CrossDesk.SessionHelper" type="win32" />
<description>CrossDesk Session Helper</description>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</windowsSettings>
</application>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
</assembly>
@@ -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"
+11
View File
@@ -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 &&
@@ -426,9 +426,8 @@ ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
}
bool ConfigCenter::IsHardwareVideoCodecAvailable() {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
!defined(__arm__) && USE_CUDA) || \
defined(__APPLE__))
#if defined(_WIN32) || defined(__APPLE__) || \
(defined(__linux__) && !defined(__aarch64__) && !defined(__arm__) && USE_CUDA)
return true;
#else
return false;
@@ -56,6 +56,7 @@
#include <windows.h>
#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<ShutdownSessionGuard> shutdown_guard;
std::wstring_view shutdown_reason;
#endif
gallery::Workspace gallery;
std::filesystem::path gallery_path;
std::vector<std::string> gallery_ids;
@@ -1284,6 +1289,10 @@ void GuiApplication::InitializeUi() {
slint::set_xdg_app_id("crossdesk");
#endif
ui_ = std::make_unique<SlintUi>();
#if _WIN32
ui_->shutdown_guard =
std::make_unique<ShutdownSessionGuard>(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
@@ -0,0 +1,121 @@
#include "shutdown_session_guard.h"
#include <utility>
#include "rd_log.h"
namespace crossdesk {
namespace {
constexpr wchar_t kShutdownWindowClass[] = L"CrossDesk.ShutdownSessionGuard";
}
ShutdownSessionGuard::ShutdownSessionGuard(
std::shared_ptr<std::atomic<bool>> 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<CREATESTRUCTW*>(lparam);
auto* guard = static_cast<ShutdownSessionGuard*>(create->lpCreateParams);
guard->window_ = window;
SetWindowLongPtrW(window, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(guard));
}
auto* guard = reinterpret_cast<ShutdownSessionGuard*>(
GetWindowLongPtrW(window, GWLP_USERDATA));
if (guard && message == WM_QUERYENDSESSION) {
const auto flags = static_cast<DWORD>(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<DWORD>(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
@@ -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 <Windows.h>
#include <atomic>
#include <memory>
#include <string>
#include <string_view>
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<std::atomic<bool>> 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<std::atomic<bool>> connected_;
HWND window_ = nullptr;
std::wstring reason_;
bool reason_registered_ = false;
bool shutdown_allowed_ = false;
ULONGLONG last_error_tick_ = 0;
};
} // namespace crossdesk
#endif
@@ -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) {
@@ -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;
@@ -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 <Windows.h>
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
+2 -5
View File
@@ -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",