mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-26 07:14:33 +08:00
[feat] add Windows pre-login remote control and fix autostart
This commit is contained in:
@@ -20,6 +20,8 @@
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "unattended_config.h"
|
||||
#include "unattended_policy.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -1007,7 +1009,7 @@ DWORD WINAPI CrossDeskServiceHost::ServiceControlHandler(
|
||||
return NO_ERROR;
|
||||
case SERVICE_CONTROL_STOP:
|
||||
case SERVICE_CONTROL_SHUTDOWN:
|
||||
instance_->ReportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 3000);
|
||||
instance_->ReportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 20000);
|
||||
instance_->RequestStop();
|
||||
return NO_ERROR;
|
||||
case SERVICE_CONTROL_SESSIONCHANGE: {
|
||||
@@ -1054,6 +1056,28 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
||||
return static_cast<int>(error);
|
||||
}
|
||||
|
||||
GUID lifetime_id{};
|
||||
wchar_t lifetime_guid[40]{};
|
||||
if (FAILED(CoCreateGuid(&lifetime_id)) ||
|
||||
!StringFromGUID2(lifetime_id, lifetime_guid, 40))
|
||||
return ERROR_GEN_FAILURE;
|
||||
lifetime_mutex_name_ =
|
||||
L"Global\\CrossDeskServiceLifetime-" + std::wstring(lifetime_guid);
|
||||
PSECURITY_DESCRIPTOR lifetime_descriptor = nullptr;
|
||||
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
L"D:P(A;;GA;;;SY)(A;;GA;;;BA)(A;;0x00100000;;;AU)", SDDL_REVISION_1,
|
||||
&lifetime_descriptor, nullptr))
|
||||
return static_cast<int>(GetLastError());
|
||||
SECURITY_ATTRIBUTES lifetime_attributes{sizeof(SECURITY_ATTRIBUTES),
|
||||
lifetime_descriptor, FALSE};
|
||||
// Owned by this service-main thread until every child has been stopped.
|
||||
lifetime_mutex_ =
|
||||
CreateMutexW(&lifetime_attributes, TRUE, lifetime_mutex_name_.c_str());
|
||||
const DWORD lifetime_error = GetLastError();
|
||||
LocalFree(lifetime_descriptor);
|
||||
if (!lifetime_mutex_ || lifetime_error == ERROR_ALREADY_EXISTS)
|
||||
return ERROR_INVALID_HANDLE;
|
||||
|
||||
started_at_tick_ = GetTickCount64();
|
||||
last_sas_tick_ = 0;
|
||||
active_session_id_ = WTSGetActiveConsoleSessionId();
|
||||
@@ -1123,9 +1147,6 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
||||
}
|
||||
|
||||
void CrossDeskServiceHost::ShutdownRuntime() {
|
||||
StopSecureInputHelper();
|
||||
StopSessionHelper();
|
||||
|
||||
if (stop_event_ != nullptr) {
|
||||
SetEvent(stop_event_);
|
||||
}
|
||||
@@ -1133,10 +1154,18 @@ void CrossDeskServiceHost::ShutdownRuntime() {
|
||||
if (client_process_monitor_thread_.joinable()) {
|
||||
client_process_monitor_thread_.join();
|
||||
}
|
||||
StopUnattendedHost();
|
||||
|
||||
if (ipc_thread_.joinable()) {
|
||||
ipc_thread_.join();
|
||||
}
|
||||
StopSecureInputHelper();
|
||||
StopSessionHelper();
|
||||
if (lifetime_mutex_) {
|
||||
ReleaseMutex(lifetime_mutex_);
|
||||
CloseHandle(lifetime_mutex_);
|
||||
lifetime_mutex_ = nullptr;
|
||||
}
|
||||
|
||||
if (stop_event_ != nullptr) {
|
||||
CloseHandle(stop_event_);
|
||||
@@ -1152,6 +1181,7 @@ void CrossDeskServiceHost::RequestStop() {
|
||||
|
||||
void CrossDeskServiceHost::ClientProcessMonitorLoop() {
|
||||
const ULONGLONG monitor_started_at = GetTickCount64();
|
||||
uint64_t logoff_generation = unattended_logoff_generation_.load();
|
||||
|
||||
while (stop_event_ != nullptr) {
|
||||
DWORD wait_result =
|
||||
@@ -1163,6 +1193,34 @@ void CrossDeskServiceHost::ClientProcessMonitorLoop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool enabled = IsUnattendedEnabled();
|
||||
const uint64_t generation = unattended_logoff_generation_.load();
|
||||
if (generation != logoff_generation) {
|
||||
logoff_generation = generation;
|
||||
StopUnattendedHost();
|
||||
// Windows may destroy/recreate Default during logoff, even when the
|
||||
// numeric console session ID does not change. Rebind on the next tick.
|
||||
continue;
|
||||
}
|
||||
if (unattended_process_ &&
|
||||
WaitForSingleObject(unattended_process_, 0) != WAIT_TIMEOUT) {
|
||||
StopUnattendedHost();
|
||||
}
|
||||
const DWORD console_session = WTSGetActiveConsoleSessionId();
|
||||
const auto action =
|
||||
UnattendedAction(enabled, console_session,
|
||||
unattended_process_ != nullptr, unattended_session_);
|
||||
if (action == UnattendedProcessAction::stop) StopUnattendedHost();
|
||||
if (action == UnattendedProcessAction::start) {
|
||||
if (!LaunchUnattendedHost(console_session)) {
|
||||
// Boot can precede creation of WinSta0 or network availability.
|
||||
WaitForSingleObject(stop_event_, 4000);
|
||||
}
|
||||
}
|
||||
// An enabled host is independent of the GUI, including while Windows is
|
||||
// between console sessions or the host is retrying startup.
|
||||
if (enabled) continue;
|
||||
|
||||
if (GetTickCount64() - monitor_started_at <
|
||||
kCrossDeskClientMonitorStartupGraceMs) {
|
||||
continue;
|
||||
@@ -1178,6 +1236,88 @@ void CrossDeskServiceHost::ClientProcessMonitorLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
bool CrossDeskServiceHost::LaunchUnattendedHost(DWORD session_id) {
|
||||
if (!IsLocalSystemProcess() || !PrepareUnattendedDirectory(false)) {
|
||||
LOG_ERROR("Unattended host requires SYSTEM and a private machine profile");
|
||||
return false;
|
||||
}
|
||||
const auto executable =
|
||||
std::filesystem::path(GetCurrentExecutablePathW()).parent_path() /
|
||||
L"CrossDesk.exe";
|
||||
GUID nonce{};
|
||||
wchar_t guid[40]{};
|
||||
if (FAILED(CoCreateGuid(&nonce)) || !StringFromGUID2(nonce, guid, 40))
|
||||
return false;
|
||||
const std::wstring event_name =
|
||||
L"Global\\CrossDeskUnattendedStop-" + std::wstring(guid);
|
||||
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
||||
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
L"D:P(A;;GA;;;SY)", SDDL_REVISION_1, &descriptor, nullptr))
|
||||
return false;
|
||||
SECURITY_ATTRIBUTES attributes{sizeof(SECURITY_ATTRIBUTES), descriptor,
|
||||
FALSE};
|
||||
HANDLE event = CreateEventW(&attributes, TRUE, FALSE, event_name.c_str());
|
||||
const DWORD event_error = GetLastError();
|
||||
LocalFree(descriptor);
|
||||
if (!event || event_error == ERROR_ALREADY_EXISTS) {
|
||||
if (event) CloseHandle(event);
|
||||
return false;
|
||||
}
|
||||
HANDLE token = nullptr;
|
||||
DWORD error = 0;
|
||||
ScopedEnvironmentBlock environment;
|
||||
PROCESS_INFORMATION process{};
|
||||
STARTUPINFOW startup{};
|
||||
startup.cb = sizeof(startup);
|
||||
wchar_t desktop[] = L"winsta0\\default";
|
||||
startup.lpDesktop = desktop;
|
||||
std::wstring command = QuoteWindowsArgument(executable.wstring()) +
|
||||
L" --unattended-host " +
|
||||
QuoteWindowsArgument(event_name) + L" " +
|
||||
QuoteWindowsArgument(lifetime_mutex_name_);
|
||||
bool created = false;
|
||||
if (CreateSessionSystemToken(session_id, &token, &error)) {
|
||||
if (CreateEnvironmentBlock(&environment.environment, token, FALSE)) {
|
||||
created = CreateProcessAsUserW(
|
||||
token, executable.c_str(), command.data(), nullptr, nullptr,
|
||||
FALSE, CREATE_UNICODE_ENVIRONMENT | CREATE_NO_WINDOW,
|
||||
environment.environment, executable.parent_path().c_str(),
|
||||
&startup, &process) != FALSE;
|
||||
}
|
||||
error = created ? ERROR_SUCCESS : GetLastError();
|
||||
CloseHandle(token);
|
||||
}
|
||||
if (process.hThread) CloseHandle(process.hThread);
|
||||
if (!created) {
|
||||
CloseHandle(event);
|
||||
LOG_WARN("Unattended host launch failed: session={}, error={}", session_id,
|
||||
error);
|
||||
return false;
|
||||
}
|
||||
unattended_process_ = process.hProcess;
|
||||
unattended_stop_event_ = event;
|
||||
unattended_session_ = session_id;
|
||||
LOG_INFO("Unattended host started: session={}, pid={}", session_id,
|
||||
process.dwProcessId);
|
||||
return true;
|
||||
}
|
||||
|
||||
void CrossDeskServiceHost::StopUnattendedHost() {
|
||||
if (unattended_stop_event_) SetEvent(unattended_stop_event_);
|
||||
if (unattended_process_) WaitForSingleObject(unattended_process_, 5000);
|
||||
if (unattended_process_ &&
|
||||
WaitForSingleObject(unattended_process_, 0) == WAIT_TIMEOUT)
|
||||
TerminateProcess(unattended_process_, ERROR_PROCESS_ABORTED);
|
||||
if (unattended_process_) {
|
||||
// Do not start another instance until the previous identity has exited.
|
||||
WaitForSingleObject(unattended_process_, INFINITE);
|
||||
CloseHandle(unattended_process_);
|
||||
}
|
||||
if (unattended_stop_event_) CloseHandle(unattended_stop_event_);
|
||||
unattended_process_ = unattended_stop_event_ = nullptr;
|
||||
unattended_session_ = 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
void CrossDeskServiceHost::ReportServiceStatus(DWORD current_state,
|
||||
DWORD win32_exit_code,
|
||||
DWORD wait_hint) {
|
||||
@@ -1226,7 +1366,7 @@ int CrossDeskServiceHost::RunServiceLoop(bool as_service) {
|
||||
|
||||
if (console_mode_) {
|
||||
SetConsoleCtrlHandler(&CrossDeskServiceHost::ConsoleControlHandler, TRUE);
|
||||
std::cout << "CrossDesk service skeleton running in console mode. Press "
|
||||
std::cout << "CrossDesk service running in console mode. Press "
|
||||
"Ctrl+C to stop."
|
||||
<< std::endl;
|
||||
}
|
||||
@@ -1234,7 +1374,7 @@ int CrossDeskServiceHost::RunServiceLoop(bool as_service) {
|
||||
WaitForSingleObject(stop_event_, INFINITE);
|
||||
|
||||
if (as_service) {
|
||||
ReportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 3000);
|
||||
ReportServiceStatus(SERVICE_STOP_PENDING, NO_ERROR, 20000);
|
||||
}
|
||||
|
||||
ShutdownRuntime();
|
||||
@@ -1448,6 +1588,9 @@ bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
|
||||
}
|
||||
|
||||
std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
||||
// Session 0's input desktop cannot describe a console that has no user
|
||||
// helper yet. Pre-login must always use the SYSTEM secure-desktop path.
|
||||
if (prelogin_) return "credential-ui";
|
||||
if (IsSasSecureDesktopGraceActiveLocked() &&
|
||||
(session_helper_report_interactive_stage_.empty() ||
|
||||
session_helper_report_interactive_stage_ == "user-desktop")) {
|
||||
@@ -1472,6 +1615,7 @@ std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
||||
|
||||
std::string CrossDeskServiceHost::ResolveInteractiveDesktopLocked(
|
||||
const std::string& interactive_stage) const {
|
||||
if (prelogin_) return "Winlogon";
|
||||
if (session_helper_status_ok_ &&
|
||||
session_helper_report_input_desktop_available_ &&
|
||||
!session_helper_report_input_desktop_.empty() &&
|
||||
@@ -1694,10 +1838,11 @@ bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring command_line = QuoteWindowsArgument(helper_path) +
|
||||
L" --session-helper --session-id " +
|
||||
std::to_wstring(session_id) + L" --stop-event " +
|
||||
QuoteWindowsArgument(stop_event_name);
|
||||
std::wstring command_line =
|
||||
QuoteWindowsArgument(helper_path) + L" --session-helper --session-id " +
|
||||
std::to_wstring(session_id) + L" --stop-event " +
|
||||
QuoteWindowsArgument(stop_event_name) + L" --service-lifetime " +
|
||||
QuoteWindowsArgument(lifetime_mutex_name_);
|
||||
std::wstring mutable_command_line = command_line;
|
||||
|
||||
STARTUPINFOW startup_info{};
|
||||
@@ -1756,6 +1901,13 @@ bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!created) {
|
||||
CloseHandle(stop_event_handle);
|
||||
LOG_ERROR("Could not launch a supervised session helper, session={}",
|
||||
session_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseHandle(process_info.hThread);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
@@ -1803,10 +1955,11 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring command_line = QuoteWindowsArgument(helper_path) +
|
||||
L" --secure-input-helper --session-id " +
|
||||
std::to_wstring(session_id) + L" --stop-event " +
|
||||
QuoteWindowsArgument(stop_event_name);
|
||||
std::wstring command_line =
|
||||
QuoteWindowsArgument(helper_path) +
|
||||
L" --secure-input-helper --session-id " + std::to_wstring(session_id) +
|
||||
L" --stop-event " + QuoteWindowsArgument(stop_event_name) +
|
||||
L" --service-lifetime " + QuoteWindowsArgument(lifetime_mutex_name_);
|
||||
std::wstring mutable_command_line = command_line;
|
||||
|
||||
STARTUPINFOW startup_info{};
|
||||
@@ -1851,6 +2004,13 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(
|
||||
}
|
||||
}
|
||||
|
||||
if (!created) {
|
||||
CloseHandle(stop_event_handle);
|
||||
LOG_ERROR("Could not launch a supervised session helper, session={}",
|
||||
session_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseHandle(process_info.hThread);
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
@@ -1985,6 +2145,10 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
|
||||
|
||||
void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
|
||||
DWORD session_id) {
|
||||
if (event_type == WTS_SESSION_LOGOFF) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
if (session_id == active_session_id_) ++unattended_logoff_generation_;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
last_session_event_type_ = event_type;
|
||||
@@ -2444,9 +2608,11 @@ bool InstallCrossDeskService(const std::wstring& binary_path) {
|
||||
}
|
||||
|
||||
std::wstring service_command = L"\"" + binary_path + L"\" --service";
|
||||
const DWORD start_type =
|
||||
IsUnattendedEnabled() ? SERVICE_AUTO_START : SERVICE_DEMAND_START;
|
||||
SC_HANDLE service = CreateServiceW(
|
||||
manager, kCrossDeskServiceName, kCrossDeskServiceDisplayName,
|
||||
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, SERVICE_DEMAND_START,
|
||||
SERVICE_ALL_ACCESS, SERVICE_WIN32_OWN_PROCESS, start_type,
|
||||
SERVICE_ERROR_NORMAL, service_command.c_str(), nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr);
|
||||
|
||||
@@ -2460,14 +2626,14 @@ bool InstallCrossDeskService(const std::wstring& binary_path) {
|
||||
|
||||
service = OpenServiceW(manager, kCrossDeskServiceName,
|
||||
SERVICE_CHANGE_CONFIG | SERVICE_QUERY_STATUS |
|
||||
READ_CONTROL | WRITE_DAC);
|
||||
SERVICE_START | READ_CONTROL | WRITE_DAC);
|
||||
if (service == nullptr) {
|
||||
LOG_ERROR("OpenServiceW failed, error={}", GetLastError());
|
||||
CloseServiceHandle(manager);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ChangeServiceConfigW(service, SERVICE_NO_CHANGE, SERVICE_DEMAND_START,
|
||||
if (!ChangeServiceConfigW(service, SERVICE_NO_CHANGE, start_type,
|
||||
SERVICE_NO_CHANGE, service_command.c_str(),
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
kCrossDeskServiceDisplayName)) {
|
||||
@@ -2484,6 +2650,21 @@ bool InstallCrossDeskService(const std::wstring& binary_path) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SC_ACTION actions[] = {{SC_ACTION_RESTART, 5000},
|
||||
{SC_ACTION_RESTART, 15000},
|
||||
{SC_ACTION_RESTART, 60000}};
|
||||
SERVICE_FAILURE_ACTIONSW recovery{};
|
||||
recovery.dwResetPeriod = 86400;
|
||||
recovery.cActions = static_cast<DWORD>(sizeof(actions) / sizeof(actions[0]));
|
||||
recovery.lpsaActions = actions;
|
||||
if (!ChangeServiceConfig2W(service, SERVICE_CONFIG_FAILURE_ACTIONS,
|
||||
&recovery)) {
|
||||
LOG_ERROR("Could not configure service recovery, error={}", GetLastError());
|
||||
CloseServiceHandle(service);
|
||||
CloseServiceHandle(manager);
|
||||
return false;
|
||||
}
|
||||
|
||||
CloseServiceHandle(service);
|
||||
CloseServiceHandle(manager);
|
||||
return true;
|
||||
@@ -2609,6 +2790,7 @@ bool StopCrossDeskService(DWORD timeout_ms) {
|
||||
}
|
||||
|
||||
bool UninstallCrossDeskService() {
|
||||
if (!SetUnattendedEnabled(false)) return false;
|
||||
SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS);
|
||||
if (manager == nullptr) {
|
||||
LOG_ERROR("OpenSCManagerW failed, error={}", GetLastError());
|
||||
@@ -2628,7 +2810,11 @@ bool UninstallCrossDeskService() {
|
||||
return false;
|
||||
}
|
||||
|
||||
StopCrossDeskService();
|
||||
if (!StopCrossDeskService()) {
|
||||
CloseServiceHandle(service);
|
||||
CloseServiceHandle(manager);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = DeleteService(service) != FALSE;
|
||||
if (!success) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <Windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -35,6 +36,8 @@ class CrossDeskServiceHost {
|
||||
void ShutdownRuntime();
|
||||
void RequestStop();
|
||||
void ClientProcessMonitorLoop();
|
||||
bool LaunchUnattendedHost(DWORD session_id);
|
||||
void StopUnattendedHost();
|
||||
void ReportServiceStatus(DWORD current_state, DWORD win32_exit_code,
|
||||
DWORD wait_hint);
|
||||
void IpcServerLoop();
|
||||
@@ -87,7 +90,14 @@ class CrossDeskServiceHost {
|
||||
HANDLE stop_event_ = nullptr;
|
||||
std::thread ipc_thread_;
|
||||
std::thread client_process_monitor_thread_;
|
||||
// Owned exclusively by the monitor, then drained after it joins.
|
||||
HANDLE unattended_process_ = nullptr;
|
||||
HANDLE unattended_stop_event_ = nullptr;
|
||||
DWORD unattended_session_ = 0xFFFFFFFF;
|
||||
std::atomic<uint64_t> unattended_logoff_generation_{0};
|
||||
std::mutex state_mutex_;
|
||||
HANDLE lifetime_mutex_ = nullptr;
|
||||
std::wstring lifetime_mutex_name_;
|
||||
DWORD active_session_id_ = 0xFFFFFFFF;
|
||||
DWORD process_session_id_ = 0xFFFFFFFF;
|
||||
DWORD input_desktop_error_code_ = 0;
|
||||
@@ -154,7 +164,7 @@ bool IsCrossDeskServiceInstalled();
|
||||
bool InstallCrossDeskService(const std::wstring& binary_path);
|
||||
bool UninstallCrossDeskService();
|
||||
bool StartCrossDeskService();
|
||||
bool StopCrossDeskService(DWORD timeout_ms = 5000);
|
||||
bool StopCrossDeskService(DWORD timeout_ms = 20000);
|
||||
std::string QueryCrossDeskService(const std::string& command,
|
||||
DWORD timeout_ms = 1000);
|
||||
std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down,
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// The service's main thread owns a Global mutex for its entire lifetime.
|
||||
// Unlike a process handle or a job created in Session 0, this can be waited
|
||||
// on by both SYSTEM and ordinary-user helpers in another Windows session.
|
||||
class ServiceLifetimeWatcher {
|
||||
public:
|
||||
ServiceLifetimeWatcher() = default;
|
||||
ServiceLifetimeWatcher(const ServiceLifetimeWatcher&) = delete;
|
||||
ServiceLifetimeWatcher& operator=(const ServiceLifetimeWatcher&) = delete;
|
||||
~ServiceLifetimeWatcher() {
|
||||
if (cancel_) SetEvent(cancel_);
|
||||
if (thread_.joinable()) thread_.join();
|
||||
if (cancel_) CloseHandle(cancel_);
|
||||
if (lifetime_) CloseHandle(lifetime_);
|
||||
}
|
||||
|
||||
bool Start(const std::wstring& name) {
|
||||
if (lifetime_ || name.empty()) return false;
|
||||
lifetime_ = OpenMutexW(SYNCHRONIZE, FALSE, name.c_str());
|
||||
if (!lifetime_) return false;
|
||||
cancel_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (!cancel_) return false;
|
||||
thread_ = std::thread([this] {
|
||||
HANDLE handles[] = {lifetime_, cancel_};
|
||||
const DWORD result = WaitForMultipleObjects(2, handles, FALSE, INFINITE);
|
||||
if (result != WAIT_OBJECT_0 + 1) {
|
||||
// Includes abandoned ownership after a service crash. Do not wait for
|
||||
// transport/capture cleanup that could otherwise leave an orphan
|
||||
// online.
|
||||
TerminateProcess(GetCurrentProcess(), ERROR_BROKEN_PIPE);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
private:
|
||||
HANDLE lifetime_ = nullptr;
|
||||
HANDLE cancel_ = nullptr;
|
||||
std::thread thread_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
@@ -26,6 +26,7 @@
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "service_lifetime.h"
|
||||
#include "usbmmidd_policy.h"
|
||||
#include "usbmmidd_virtual_display.h"
|
||||
#include "windows_cursor_state.h"
|
||||
@@ -1967,6 +1968,7 @@ int main(int argc, char* argv[]) {
|
||||
bool run_secure_input_helper = false;
|
||||
DWORD expected_session_id = 0xFFFFFFFF;
|
||||
std::wstring stop_event_name;
|
||||
std::wstring service_lifetime_name;
|
||||
for (int index = 1; index < argc; ++index) {
|
||||
std::string argument = argv[index];
|
||||
if (argument == "--session-helper") {
|
||||
@@ -1985,6 +1987,10 @@ int main(int argc, char* argv[]) {
|
||||
stop_event_name = Utf8ToWide(argv[++index]);
|
||||
continue;
|
||||
}
|
||||
if (argument == "--service-lifetime" && index + 1 < argc) {
|
||||
service_lifetime_name = Utf8ToWide(argv[++index]);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!run_helper && !run_secure_input_helper) {
|
||||
@@ -1993,6 +1999,9 @@ int main(int argc, char* argv[]) {
|
||||
}
|
||||
|
||||
DWORD current_session_id = 0xFFFFFFFF;
|
||||
crossdesk::ServiceLifetimeWatcher lifetime;
|
||||
if (!service_lifetime_name.empty() && !lifetime.Start(service_lifetime_name))
|
||||
return 1;
|
||||
ProcessIdToSessionId(GetCurrentProcessId(), ¤t_session_id);
|
||||
HANDLE stop_event = nullptr;
|
||||
if (!stop_event_name.empty()) {
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
#include "unattended_config.h"
|
||||
|
||||
#include <Aclapi.h>
|
||||
#include <ShlObj.h>
|
||||
#include <sddl.h>
|
||||
|
||||
#include <ctime>
|
||||
#include <fstream>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
constexpr wchar_t kRegistryKey[] = L"SOFTWARE\\CrossDesk\\Unattended";
|
||||
constexpr wchar_t kDirectorySecurity[] =
|
||||
L"O:BAG:BAD:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)";
|
||||
|
||||
bool TrustedSid(PSID sid) {
|
||||
return sid && (IsWellKnownSid(sid, WinLocalSystemSid) ||
|
||||
IsWellKnownSid(sid, WinBuiltinAdministratorsSid));
|
||||
}
|
||||
|
||||
bool PrivateDirectory(const std::filesystem::path& path) {
|
||||
const DWORD attributes = GetFileAttributesW(path.c_str());
|
||||
if (attributes == INVALID_FILE_ATTRIBUTES ||
|
||||
!(attributes & FILE_ATTRIBUTE_DIRECTORY) ||
|
||||
(attributes & FILE_ATTRIBUTE_REPARSE_POINT))
|
||||
return false;
|
||||
PSID owner = nullptr;
|
||||
PACL dacl = nullptr;
|
||||
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
||||
if (GetNamedSecurityInfoW(
|
||||
const_cast<wchar_t*>(path.c_str()), SE_FILE_OBJECT,
|
||||
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION, &owner,
|
||||
nullptr, &dacl, nullptr, &descriptor) != ERROR_SUCCESS)
|
||||
return false;
|
||||
bool safe = TrustedSid(owner) && dacl && dacl->AceCount > 0;
|
||||
for (DWORD index = 0; safe && index < dacl->AceCount; ++index) {
|
||||
void* raw = nullptr;
|
||||
if (!GetAce(dacl, index, &raw)) {
|
||||
safe = false;
|
||||
break;
|
||||
}
|
||||
auto* ace = static_cast<ACCESS_ALLOWED_ACE*>(raw);
|
||||
// Only SYSTEM and elevated administrators can read credentials or write
|
||||
// configuration that the service will consume as SYSTEM.
|
||||
safe =
|
||||
ace->Header.AceType == ACCESS_ALLOWED_ACE_TYPE &&
|
||||
TrustedSid(&ace->SidStart) &&
|
||||
(ace->Header.AceFlags & (OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)) ==
|
||||
(OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE);
|
||||
}
|
||||
LocalFree(descriptor);
|
||||
return safe;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
std::filesystem::path UnattendedDataPath() {
|
||||
PWSTR value = nullptr;
|
||||
if (FAILED(SHGetKnownFolderPath(FOLDERID_ProgramData, KF_FLAG_DEFAULT,
|
||||
nullptr, &value)))
|
||||
return {};
|
||||
const auto path = std::filesystem::path(value) / L"CrossDeskUnattended";
|
||||
CoTaskMemFree(value);
|
||||
return path;
|
||||
}
|
||||
|
||||
bool IsUnattendedEnabled() {
|
||||
DWORD enabled = 0;
|
||||
DWORD size = sizeof(enabled);
|
||||
return RegGetValueW(HKEY_LOCAL_MACHINE, kRegistryKey, L"Enabled",
|
||||
RRF_RT_REG_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr,
|
||||
&enabled, &size) == ERROR_SUCCESS &&
|
||||
enabled == 1;
|
||||
}
|
||||
|
||||
bool SetUnattendedEnabled(bool enabled) {
|
||||
if (!IsAdministratorProcess()) return false;
|
||||
HKEY key = nullptr;
|
||||
if (RegCreateKeyExW(HKEY_LOCAL_MACHINE, kRegistryKey, 0, nullptr, 0,
|
||||
KEY_SET_VALUE | KEY_WOW64_64KEY, nullptr, &key,
|
||||
nullptr) != ERROR_SUCCESS)
|
||||
return false;
|
||||
const DWORD value = enabled ? 1 : 0;
|
||||
const auto result =
|
||||
RegSetValueExW(key, L"Enabled", 0, REG_DWORD,
|
||||
reinterpret_cast<const BYTE*>(&value), sizeof(value));
|
||||
RegCloseKey(key);
|
||||
return result == ERROR_SUCCESS;
|
||||
}
|
||||
|
||||
bool IsAdministratorProcess() {
|
||||
BYTE sid[SECURITY_MAX_SID_SIZE];
|
||||
DWORD size = sizeof(sid);
|
||||
BOOL member = FALSE;
|
||||
return CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, sid, &size) &&
|
||||
CheckTokenMembership(nullptr, sid, &member) && member;
|
||||
}
|
||||
|
||||
bool IsLocalSystemProcess() {
|
||||
HANDLE token = nullptr;
|
||||
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return false;
|
||||
DWORD size = 0;
|
||||
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
|
||||
std::vector<BYTE> buffer(size);
|
||||
const bool system =
|
||||
GetTokenInformation(token, TokenUser, buffer.data(), size, &size) &&
|
||||
IsWellKnownSid(reinterpret_cast<TOKEN_USER*>(buffer.data())->User.Sid,
|
||||
WinLocalSystemSid);
|
||||
CloseHandle(token);
|
||||
return system;
|
||||
}
|
||||
|
||||
bool PrepareUnattendedDirectory(bool create) {
|
||||
return unattended_detail::PreparePrivateDirectory(UnattendedDataPath(),
|
||||
create);
|
||||
}
|
||||
|
||||
bool unattended_detail::PreparePrivateDirectory(
|
||||
const std::filesystem::path& root, bool create) {
|
||||
if (root.empty()) return false;
|
||||
if (create) {
|
||||
PSECURITY_DESCRIPTOR descriptor = nullptr;
|
||||
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
|
||||
kDirectorySecurity, SDDL_REVISION_1, &descriptor, nullptr))
|
||||
return false;
|
||||
SECURITY_ATTRIBUTES attributes{sizeof(SECURITY_ATTRIBUTES), descriptor,
|
||||
FALSE};
|
||||
const bool created = CreateDirectoryW(root.c_str(), &attributes) != FALSE;
|
||||
const DWORD error = created ? ERROR_SUCCESS : GetLastError();
|
||||
LocalFree(descriptor);
|
||||
if (!created && error != ERROR_ALREADY_EXISTS) return false;
|
||||
}
|
||||
return PrivateDirectory(root);
|
||||
}
|
||||
|
||||
bool WriteUnattendedStatus(const std::string& identity, bool online,
|
||||
DWORD session_id) {
|
||||
const auto root = UnattendedDataPath();
|
||||
if (root.empty()) return false;
|
||||
const auto separator = identity.find('@');
|
||||
nlohmann::json status = {
|
||||
{"online", online},
|
||||
{"process_id", GetCurrentProcessId()},
|
||||
{"session_id", session_id},
|
||||
{"updated_at", std::time(nullptr)},
|
||||
{"device_id",
|
||||
separator == std::string::npos ? "" : identity.substr(0, separator)},
|
||||
{"password",
|
||||
separator == std::string::npos ? "" : identity.substr(separator + 1)}};
|
||||
const auto temporary = root / L"status.tmp";
|
||||
std::ofstream output(temporary, std::ios::binary | std::ios::trunc);
|
||||
output << status.dump(2);
|
||||
output.close();
|
||||
return !output.fail() &&
|
||||
MoveFileExW(temporary.c_str(), (root / L"status.json").c_str(),
|
||||
MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH);
|
||||
}
|
||||
|
||||
std::string ReadUnattendedStatus() {
|
||||
nlohmann::json status = {{"enabled", IsUnattendedEnabled()},
|
||||
{"online", false}};
|
||||
if (!IsAdministratorProcess() || !PrepareUnattendedDirectory(false)) {
|
||||
status["error"] = "administrator_required_or_private_directory_unavailable";
|
||||
return status.dump(2);
|
||||
}
|
||||
std::ifstream input(UnattendedDataPath() / L"status.json", std::ios::binary);
|
||||
auto saved = nlohmann::json::parse(input, nullptr, false);
|
||||
if (saved.is_object()) {
|
||||
status.update(saved);
|
||||
const auto updated = status.value("updated_at", std::time_t{0});
|
||||
const auto now = std::time(nullptr);
|
||||
HANDLE process =
|
||||
OpenProcess(SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, FALSE,
|
||||
status.value("process_id", 0u));
|
||||
const bool running =
|
||||
process && WaitForSingleObject(process, 0) == WAIT_TIMEOUT;
|
||||
if (process) CloseHandle(process);
|
||||
status["online"] = IsUnattendedEnabled() && running && updated <= now &&
|
||||
now - updated < 10 && status.value("online", false);
|
||||
}
|
||||
return status.dump(2);
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,27 @@
|
||||
#pragma once
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// This directory is deliberately separate from every user's GUI profile.
|
||||
std::filesystem::path UnattendedDataPath();
|
||||
bool IsUnattendedEnabled();
|
||||
bool SetUnattendedEnabled(bool enabled);
|
||||
bool IsLocalSystemProcess();
|
||||
bool IsAdministratorProcess();
|
||||
// Reject reparse points, untrusted owners and permissive existing DACLs.
|
||||
bool PrepareUnattendedDirectory(bool create);
|
||||
namespace unattended_detail {
|
||||
// Parameterized only for isolated native ACL tests; production always uses
|
||||
// the fixed ProgramData path above.
|
||||
bool PreparePrivateDirectory(const std::filesystem::path& root, bool create);
|
||||
} // namespace unattended_detail
|
||||
bool WriteUnattendedStatus(const std::string& identity, bool online,
|
||||
DWORD session_id);
|
||||
std::string ReadUnattendedStatus();
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,24 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
enum class UnattendedProcessAction { keep, start, stop };
|
||||
|
||||
// No logged-in-user condition: a console at the sign-in screen is a valid
|
||||
// target. Lock/unlock and GUI presence must not interrupt an existing host.
|
||||
inline UnattendedProcessAction UnattendedAction(bool enabled,
|
||||
uint32_t console_session,
|
||||
bool running,
|
||||
uint32_t process_session) {
|
||||
constexpr uint32_t unavailable = 0xFFFFFFFFu;
|
||||
const bool target =
|
||||
enabled && console_session != unavailable && console_session != 0;
|
||||
if (running)
|
||||
return target && process_session == console_session
|
||||
? UnattendedProcessAction::keep
|
||||
: UnattendedProcessAction::stop;
|
||||
return target ? UnattendedProcessAction::start
|
||||
: UnattendedProcessAction::keep;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
Reference in New Issue
Block a user