mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-26 07:14:33 +08:00
[fix] preserve Windows remote sessions during shutdown
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user