[feat] add Windows pre-login remote control and fix autostart

This commit is contained in:
dijunkun
2026-10-04 12:49:26 +08:00
parent b8f704abb9
commit 7f7b91851e
33 changed files with 1902 additions and 70 deletions
+26
View File
@@ -445,6 +445,25 @@ jobs:
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Test installed Windows autostart and unattended host
run: |
xmake b -vy windows_autostart_test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
xmake run windows_autostart_test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
xmake b -vy windows_unattended_policy_test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
xmake b -vy windows_unattended_security_test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
xmake run windows_unattended_policy_test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
xmake run windows_unattended_security_test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
xmake b -vy windows_service_lifetime_test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
xmake run windows_service_lifetime_test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Package
shell: pwsh
run: |
@@ -501,6 +520,13 @@ jobs:
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true --CROSSDESK_PORTABLE=true -y
xmake b -vy crossdesk
- name: Test portable Windows autostart
run: |
xmake b -vy windows_autostart_test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
xmake run windows_autostart_test
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Package Portable
shell: pwsh
run: |
+26 -1
View File
@@ -191,7 +191,32 @@ sudo apt install "./crossdesk-linux-amd64-<version>.deb"
**CrossDesk Service**(服务名 `CrossDeskService`)提供锁屏、登录、凭据输入及安全桌面的状态上报、`Ctrl+Alt+Del` 发送和键鼠输入转发。
安装版会注册按需启动的服务,客户端启动时尝试启动它;本机没有 CrossDesk 客户端进程后服务会自动退出。便携版可在提示窗口或“设置 → 锁屏控制服务”中安装服务,需要管理员权限。该服务依赖客户端运行。
安装版默认注册按需启动的服务,客户端启动时尝试启动它;未启用无人值守时,本机没有 CrossDesk 客户端进程后服务会自动退出。便携版可在提示窗口或“设置 → 锁屏控制服务”中安装服务,需要管理员权限。
Windows 的“开机自启”在当前用户登录后启动客户端。安装版使用以最高权限运行的登录计划任务,后续登录无需再次确认 UAC;便携版和 Debug 构建使用当前用户的普通启动项。升级安装版或手动启动新版时,会自动迁移指向该程序的旧启动项。关闭自启会删除相应注册。登录前远控需要另外启用下面的无人值守模式。
### 开机后尚未登录时远控
安装版支持独立的无人值守被控端。在完整安装目录中打开**管理员 PowerShell**,一次性启用:
```powershell
.\CrossDesk.exe --unattended-enable
# 等待联网注册后,查看独立设备 ID、密码和 online 状态
.\CrossDesk.exe --unattended-status
```
使用返回的 `device_id` 和 `password` 从另一台设备连接。首次启用时复制当前客户端的服务器和编码设置,单独生成无人值守凭据;它与普通客户端的设备 ID 不同,重启、注销和重新启用时保留。后续执行启用命令会复用机器配置,不会覆盖它。自建服务器的根证书需要安装到本地计算机的信任存储中。
启用后,`CrossDeskService` 改为系统启动时自动运行,无需 Windows 自动登录,也不需要保存 Windows 登录密码。服务以 SYSTEM 身份在当前控制台会话中启动无界面的被控进程,复用登录界面抓屏、键鼠及 `Ctrl+Alt+Del` 支持。用户登录、锁屏和关闭普通客户端不会停止它;注销、切换用户或进程崩溃后服务会重新拉起,期间连接可能中断,需要重新连接同一设备 ID。启动时网络尚未就绪会继续重试。
无人值守模式提供画面、键鼠、多屏切换和安全桌面控制;不提供声音、文件传输、剪贴板或隐私遮罩。便携版和 Debug 构建不能启用此模式。设置和凭据位于 `%ProgramData%\CrossDeskUnattended`,只允许 SYSTEM 和管理员访问;日志位于其 `logs` 子目录。
```powershell
# 停止无人值守被控端,并恢复服务按需启动
.\CrossDesk.exe --unattended-disable
```
关闭普通“开机自启”不会关闭无人值守模式。禁用或卸载服务会取消无人值守启动,但保留受保护的机器配置;若需彻底清除凭据,在服务停止后由管理员删除该目录。
<details>
<summary>手动部署与服务管理命令</summary>
+26 -1
View File
@@ -183,7 +183,32 @@ Gestures include one-finger click, two-finger right-click, long-press drag, and
**CrossDesk Service** (`CrossDeskService`) reports protected desktop states and forwards `Ctrl+Alt+Del`, keyboard, and mouse input on lock screens, sign-in screens, credential prompts, and secure desktops.
The installer registers an on-demand service. CrossDesk tries to start it on launch, and the service exits when no local CrossDesk client is running. Portable builds offer service installation in the prompt or **Settings → Lock Screen Service**, with administrator privileges. The service depends on a running client.
The installer registers an on-demand service by default. CrossDesk tries to start it on launch. Unless unattended mode is enabled, the service exits when no local CrossDesk client is running. Portable builds offer service installation in the prompt or **Settings → Lock Screen Service**, with administrator privileges.
On Windows, **Auto Start** launches the client after the current user signs in. Installed release builds use a logon scheduled task with highest privileges, so subsequent logons need no UAC confirmation; portable and Debug builds use a normal per-user startup entry. Upgrading the installed build or launching the new version manually migrates a legacy startup entry pointing to that executable. Turning Auto Start off removes its registration. Remote access before sign-in requires the separate unattended mode below.
### Remote access before Windows sign-in
The installed release supports a separate unattended host. Open an **administrator PowerShell** in the complete installation directory and enable it once:
```powershell
.\CrossDesk.exe --unattended-enable
# After network registration, show its separate device ID, password and online state
.\CrossDesk.exe --unattended-status
```
Connect from another device using the returned `device_id` and `password`. First-time setup copies the current client's server and codec preferences and creates separate credentials. This ID differs from the GUI client's ID and persists across reboots, logoffs and re-enabling. Subsequent enable commands retain the machine configuration. Self-hosted server root certificates must be trusted in the Local Computer certificate store.
Once enabled, `CrossDeskService` starts automatically at system boot. Windows automatic login and saved Windows login passwords are unnecessary. The service launches a windowless SYSTEM host in the active console session and reuses secure-desktop capture, keyboard/mouse input and `Ctrl+Alt+Del`. Signing in, locking Windows or closing the ordinary client does not stop it. Logoff, user switching or a host crash causes the service to restart the host; the connection may drop, and you can reconnect using the same ID. Network connections retry if networking is unavailable during boot.
Unattended mode supports screen viewing, keyboard/mouse input, monitor switching and secure desktops. Audio, file transfer, clipboard and privacy screens are unavailable. Portable and Debug builds cannot enable it. Configuration and credentials reside in `%ProgramData%\CrossDeskUnattended`, accessible only to SYSTEM and administrators; logs are in its `logs` subdirectory.
```powershell
# Stop the unattended host and restore on-demand service startup
.\CrossDesk.exe --unattended-disable
```
Turning off ordinary Auto Start does not disable unattended mode. Disabling or uninstalling the service disarms unattended startup but retains the protected machine profile. To erase its credentials, stop the service and delete that directory as an administrator.
<details>
<summary>Manual deployment and service commands</summary>
+31 -1
View File
@@ -101,6 +101,15 @@ installApp:
Call RegisterInstalledService
; Replace an existing per-user Run entry only after the new binary is in
; place. A fresh installation must not opt the user into auto-start.
ClearErrors
ExecWait '"$INSTDIR\CrossDesk.exe" --autostart-migrate' $0
${If} ${Errors}
${OrIf} $0 != 0
MessageBox MB_ICONEXCLAMATION|MB_OK "Could not migrate CrossDesk auto-start. Open CrossDesk and enable Auto Start again."
${EndIf}
; Write uninstall information
WriteUninstaller "$INSTDIR\uninstall.exe"
@@ -147,6 +156,12 @@ cancelUninstall:
Abort
uninstallApp:
ClearErrors
ExecWait '"$INSTDIR\CrossDesk.exe" --autostart-uninstall' $0
${If} ${Errors}
${OrIf} $0 != 0
MessageBox MB_ICONEXCLAMATION|MB_OK "Could not remove CrossDesk auto-start. Remove any remaining CrossDesk Autostart tasks in Task Scheduler."
${EndIf}
Call un.UnregisterInstalledService
Call un.RemoveUsbmmiddDriver
@@ -218,7 +233,18 @@ Function RegisterInstalledService
Abort
${EndIf}
DetailPrint "CrossDesk service registered for on-demand start"
; Preserve an existing unattended opt-in and resume it after upgrades.
; A fresh install still registers the ordinary on-demand lock-screen service.
SetRegView 64
ReadRegDWORD $1 HKLM "Software\CrossDesk\Unattended" "Enabled"
SetRegView 32
${If} $1 = 1
ExecWait '"$INSTDIR\CrossDesk.exe" --service-start' $0
${If} $0 != 0
MessageBox MB_ICONEXCLAMATION|MB_OK "Could not restart the unattended host. Run CrossDesk.exe --service-start from an administrator terminal."
${EndIf}
${EndIf}
DetailPrint "CrossDesk service registered"
Return
@@ -228,6 +254,10 @@ missing_service_binary:
FunctionEnd
Function un.UnregisterInstalledService
; Also disarm unattended startup when removal must fall back to sc.exe.
SetRegView 64
DeleteRegKey HKLM "Software\CrossDesk\Unattended"
SetRegView 32
IfFileExists "$INSTDIR\CrossDesk.exe" 0 unregister_with_sc
DetailPrint "Stopping CrossDesk service"
+142
View File
@@ -16,8 +16,11 @@
#ifdef _WIN32
#include <cstdio>
#include "platform/windows/autostart_task.h"
#include "platform/windows/gui/slint_backend.h"
#include "service_host.h"
#include "service_lifetime.h"
#include "unattended_config.h"
#endif
#include "config_center.h"
@@ -70,6 +73,12 @@ void PrintServiceCliUsage() {
<< " --service-ping Ping the service over named pipe IPC\n"
<< " --service-status Query service runtime status\n"
<< " --service-help Show this help\n";
std::cout
<< " --unattended-enable Enable a separate machine host before login\n"
<< " --unattended-disable Stop the machine host and disable boot "
"startup\n"
<< " --unattended-status Show machine host status and connection "
"credentials\n";
}
std::wstring GetCurrentExecutablePathW() {
@@ -97,6 +106,9 @@ bool IsServiceCliCommand(const char* arg) {
}
return std::strcmp(arg, "--service-install") == 0 ||
std::strcmp(arg, "--unattended-enable") == 0 ||
std::strcmp(arg, "--unattended-disable") == 0 ||
std::strcmp(arg, "--unattended-status") == 0 ||
std::strcmp(arg, "--service-uninstall") == 0 ||
std::strcmp(arg, "--service-start") == 0 ||
std::strcmp(arg, "--service-stop") == 0 ||
@@ -127,6 +139,97 @@ int HandleServiceCliCommand(const std::string& command) {
return 0;
}
if (command.rfind("--unattended-", 0) == 0) {
if (!crossdesk::IsAdministratorProcess()) {
std::cerr << "Run this command from an administrator terminal.\n";
return 1;
}
if (command == "--unattended-status") {
std::cout << crossdesk::ReadUnattendedStatus() << '\n';
return 0;
}
const auto service = GetSiblingServiceExecutablePath();
if (service.empty() || !std::filesystem::exists(service)) {
std::cerr << "Install CrossDesk with its Windows service first.\n";
return 1;
}
if (command == "--unattended-disable") {
// Disarm boot startup even if stopping the current process fails.
const bool saved = crossdesk::SetUnattendedEnabled(false);
const bool configured =
saved && crossdesk::InstallCrossDeskService(service.wstring());
const bool stopped = crossdesk::StopCrossDeskService(20000);
std::cout << (configured && stopped
? "Unattended host disabled.\n"
: "Failed to disable unattended host.\n");
return configured && stopped ? 0 : 1;
}
#if defined(CROSSDESK_DEBUG) || CROSSDESK_PORTABLE
std::cerr << "Unattended mode requires the installed release build.\n";
return 1;
#else
const bool was_enabled = crossdesk::IsUnattendedEnabled();
if (!crossdesk::PrepareUnattendedDirectory(true)) {
std::cerr
<< "Cannot create a private machine profile. Existing permissive "
"directories and links are rejected.\n";
return 1;
}
if (crossdesk::IsCrossDeskServiceInstalled() &&
!crossdesk::StopCrossDeskService(20000)) {
std::cerr << "Could not stop the service to configure unattended mode.\n";
return 1;
}
crossdesk::PathManager paths("CrossDesk");
crossdesk::ConfigCenter source(
(paths.GetCachePath() / "config.ini").string());
const auto machine_config = crossdesk::UnattendedDataPath() / "config.ini";
const auto staged_config =
crossdesk::UnattendedDataPath() / "config.pending.ini";
// Import only server/media preferences. Never share the GUI identity or
// consume its file-transfer paths in the SYSTEM process.
const auto host = source.IsSelfHosted() ? source.GetSignalServerHost()
: source.GetDefaultServerHost();
const int port = source.IsSelfHosted()
? source.GetSignalServerPort()
: source.GetDefaultSignalServerPort();
bool configured = std::filesystem::exists(machine_config);
if (!configured) {
crossdesk::ConfigCenter destination(staged_config.string());
configured =
destination.SetServerHost(host) == 0 &&
destination.SetServerPort(port) == 0 &&
destination.SetSelfHosted(source.IsSelfHosted()) == 0 &&
destination.SetTurnMode(source.GetTurnMode()) == 0 &&
destination.SetVideoEncodeFormat(source.GetVideoEncodeFormat()) ==
0 &&
destination.SetHardwareVideoCodec(source.IsHardwareVideoCodec()) ==
0 &&
destination.SetScreenCaptureMethod(
crossdesk::ScreenCaptureMethod::Auto) == 0 &&
destination.SetPrivacyScreen(false) == 0 &&
destination.SetSaveRemotePreviews(false) == 0;
}
if (configured && !std::filesystem::exists(machine_config)) {
configured = MoveFileExW(staged_config.c_str(), machine_config.c_str(),
MOVEFILE_WRITE_THROUGH) != FALSE;
}
if (!configured || !crossdesk::SetUnattendedEnabled(true) ||
!crossdesk::InstallCrossDeskService(service.wstring()) ||
!crossdesk::StartCrossDeskService()) {
crossdesk::SetUnattendedEnabled(was_enabled);
crossdesk::InstallCrossDeskService(service.wstring());
if (was_enabled) crossdesk::StartCrossDeskService();
std::cerr << "Failed to enable unattended host.\n";
return 1;
}
std::cout << "Unattended host enabled for boot startup.\n"
"After it connects, use --unattended-status to obtain its "
"separate device ID and password.\n";
return 0;
#endif
}
if (command == "--service-install") {
std::filesystem::path service_path = GetSiblingServiceExecutablePath();
if (service_path.empty()) {
@@ -218,6 +321,10 @@ static int RunApplication(
}
#ifdef _WIN32
if (!crossdesk::platform::MigrateLegacyAutostart("CrossDesk")) {
crossdesk::get_logger()->warn(
"Could not migrate the legacy Windows autostart entry");
}
TryStartManagedWindowsService();
#endif
@@ -288,6 +395,41 @@ int main(int argc, char* argv[]) {
}
#endif
#ifdef _WIN32
if (argc > 1 && std::strcmp(argv[1], "--unattended-host") == 0) {
// Internal service entry point: never elevate an ordinary invocation into
// a SYSTEM host, and never open the GUI or the per-user daemon here.
if (argc != 4 || !crossdesk::IsLocalSystemProcess() ||
!crossdesk::IsUnattendedEnabled())
return 1;
const std::string name(argv[2]);
if (name.rfind("Global\\CrossDeskUnattendedStop-", 0) != 0) return 1;
const std::wstring wide_name(name.begin(), name.end());
HANDLE stop = OpenEventW(SYNCHRONIZE, FALSE, wide_name.c_str());
if (!stop) return 1;
const std::string lifetime_name(argv[3]);
crossdesk::ServiceLifetimeWatcher lifetime;
if (!lifetime.Start(
std::wstring(lifetime_name.begin(), lifetime_name.end()))) {
CloseHandle(stop);
return 1;
}
crossdesk::Render render;
const int result = render.RunUnattended(stop);
CloseHandle(stop);
return result;
}
// Installer maintenance must not start the GUI or the process supervisor.
if (argc == 2 && (std::strcmp(argv[1], "--autostart-migrate") == 0 ||
std::strcmp(argv[1], "--autostart-uninstall") == 0)) {
crossdesk::PathManager paths("CrossDesk");
crossdesk::InitLogger(paths.GetLogPath().string(),
"crossdesk-autostart-cli");
const bool success =
std::strcmp(argv[1], "--autostart-migrate") == 0
? crossdesk::platform::MigrateLegacyAutostart("CrossDesk")
: crossdesk::platform::RemoveInstalledAutostart("CrossDesk");
return success ? 0 : 1;
}
if (argc == 2 &&
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
return crossdesk::RunSlintRendererProbe();
@@ -312,7 +312,16 @@ int ConfigCenter::SetSelfHosted(bool enabled) {
int ConfigCenter::SetAutostart(bool enabled) {
const bool previous = IsAutostartEnabled("CrossDesk");
if (enabled != previous && !(enabled ? EnableAutostart("CrossDesk") : DisableAutostart("CrossDesk"))) {
// Refresh the executable path and remove legacy registrations even when the
// boolean state is unchanged. In particular, a Run entry no longer counts
// as enabled for an elevated Windows build, but still needs deleting.
#ifdef _WIN32
const bool update_registration = true;
#else
const bool update_registration = enabled != previous;
#endif
if (update_registration &&
!(enabled ? EnableAutostart("CrossDesk") : DisableAutostart("CrossDesk"))) {
LOG_ERROR("SetAutostart failed");
return -1;
}
@@ -29,6 +29,9 @@ public:
~GuiApplication();
int Run();
#ifdef _WIN32
int RunUnattended(void* stop_event);
#endif
private:
struct SlintUi;
@@ -115,7 +115,7 @@ void SessionDeviceManager::RecordCaptureCadence(
}
void SessionDeviceManager::Initialize() {
InitializeAudioOutput();
if (!owner_.unattended_host_) InitializeAudioOutput();
screen_capturer_factory_ = new ScreenCapturerFactory();
device_controller_factory_ = new DeviceControllerFactory();
keyboard_capturer_ =
@@ -329,6 +329,7 @@ void SessionDeviceManager::ReleaseRemoteMouseButtons() {
#endif
void SessionDeviceManager::StartSpeakerCapturer() {
if (owner_.unattended_host_) return;
speaker_capture_.SetEnabled(true);
}
@@ -229,6 +229,13 @@ bool SettingsManager::ReadV2Locked() {
return true;
}
std::string SettingsManager::ActiveIdentity() const {
std::lock_guard<std::mutex> lock(cache_mutex_);
return owner_.config_center_->IsSelfHosted()
? cache_v2_.self_hosted_id
: cache_v2_.client_id_with_password;
}
int SettingsManager::Load() {
std::unique_lock<std::mutex> lock(cache_mutex_);
@@ -20,6 +20,7 @@ public:
int Save();
int Load();
std::string ActiveIdentity() const;
int LoadRecentConnectionAliases();
int SaveRecentConnectionAliases() const;
+5
View File
@@ -11,5 +11,10 @@ Render::Render() : application_(std::make_unique<GuiApplication>()) {}
Render::~Render() = default;
int Render::Run() { return application_->Run(); }
#ifdef _WIN32
int Render::RunUnattended(void* stop_event) {
return application_->RunUnattended(stop_event);
}
#endif
} // namespace crossdesk
+3
View File
@@ -24,6 +24,9 @@ public:
Render &operator=(const Render &) = delete;
int Run();
#ifdef _WIN32
int RunUnattended(void* stop_event);
#endif
private:
std::unique_ptr<GuiApplication> application_;
@@ -100,6 +100,8 @@ class GuiRuntime : protected gui_detail::GuiState {
#endif
PrivacyController privacy_;
// Set before initializing callbacks; immutable for the process lifetime.
bool unattended_host_ = false;
AnnouncementInbox announcements_;
PrivacySessionPolicy privacy_sessions_;
std::mutex privacy_commands_mutex_;
@@ -91,6 +91,13 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
std::string source_id = std::string(src_id, src_id_size);
// The service-owned host runs as SYSTEM. Do not expose filesystem or
// clipboard operations in that security context.
if (runtime->unattended_host_ &&
(source_id == runtime->file_label_ ||
source_id == runtime->file_feedback_label_ ||
source_id == runtime->clipboard_label_))
return;
if (source_id == runtime->file_label_) {
std::string remote_user_id = std::string(user_id, user_id_size);
@@ -209,6 +216,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
return;
}
if (remote_action.type == ControlType::privacy_command) {
if (runtime->unattended_host_) return;
if (source_id == runtime->control_data_label_)
runtime->QueuePrivacyCommand(remote_id, remote_action.pc);
return;
@@ -278,8 +278,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
{
RemoteAction remote_action = MakeHostInformation(
GetHostName(), runtime->devices_.host_display_list(),
/*supports_privacy_screen=*/true, CROSSDESK_VERSION,
kClientPlatform);
/*supports_privacy_screen=*/!runtime->unattended_host_,
CROSSDESK_VERSION, kClientPlatform);
remote_action.i.supports_video_settings = true;
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
@@ -413,8 +413,8 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
{
RemoteAction remote_action = MakeHostInformation(
GetHostName(), runtime->devices_.host_display_list(),
/*supports_privacy_screen=*/true, CROSSDESK_VERSION,
kClientPlatform);
/*supports_privacy_screen=*/!runtime->unattended_host_,
CROSSDESK_VERSION, kClientPlatform);
remote_action.i.supports_video_settings = true;
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
@@ -201,7 +201,7 @@ void PeerEventHandler::OnReceiveAudioBuffer(
auto callback = handler->EnterCallback();
if (!callback) return;
GuiRuntime* runtime = handler ? &handler->owner_ : nullptr;
if (!runtime) {
if (!runtime || runtime->unattended_host_) {
return;
}
+293 -40
View File
@@ -1,69 +1,322 @@
#include "platform/autostart_backend.h"
#include <windows.h>
#include <sddl.h>
#include <taskschd.h>
#include <wrl/client.h>
#include <filesystem>
#include <memory>
#include <string>
#include <vector>
#include "autostart_task.h"
#include "rd_log.h"
namespace crossdesk::platform {
namespace {
constexpr const char* kRunKey =
"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
using Microsoft::WRL::ComPtr;
using Bstr = std::unique_ptr<OLECHAR, decltype(&SysFreeString)>;
constexpr wchar_t kRunKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
Bstr MakeBstr(const std::wstring& value) {
return Bstr(SysAllocString(value.c_str()), SysFreeString);
}
std::wstring FromUtf8(const std::string& value) {
if (value.empty()) return {};
const int size = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
value.c_str(), -1, nullptr, 0);
if (size <= 0) return {};
std::wstring result(size, L'\0');
if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, value.c_str(), -1,
result.data(), size))
return {};
result.pop_back();
return result;
}
std::wstring CurrentUserSid() {
HANDLE token = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return {};
DWORD size = 0;
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
std::vector<BYTE> data(size);
const bool read =
GetTokenInformation(token, TokenUser, data.data(), size, &size) != FALSE;
CloseHandle(token);
if (!read) return {};
LPWSTR sid = nullptr;
if (!ConvertSidToStringSidW(
reinterpret_cast<TOKEN_USER*>(data.data())->User.Sid, &sid))
return {};
const std::wstring result(sid);
LocalFree(sid);
return result;
}
bool Check(HRESULT result, const char* operation) {
if (SUCCEEDED(result)) return true;
LOG_ERROR("Windows autostart {} failed: HRESULT=0x{:08x}", operation,
static_cast<unsigned long>(result));
return false;
}
class ComApartment {
public:
ComApartment() : result_(CoInitializeEx(nullptr, COINIT_MULTITHREADED)) {}
~ComApartment() {
if (SUCCEEDED(result_)) CoUninitialize();
}
bool ready() const {
// Settings can run on a UI thread that already uses a different apartment.
return result_ == RPC_E_CHANGED_MODE || Check(result_, "initialize COM");
}
private:
HRESULT result_;
};
class TaskScheduler {
private:
// COM interfaces must be released before the apartment is uninitialized.
ComApartment apartment_;
ComPtr<ITaskService> service_;
public:
ComPtr<ITaskFolder> folder;
bool Open() {
if (!apartment_.ready()) return false;
if (!Check(
CoCreateInstance(CLSID_TaskScheduler, nullptr, CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(service_.GetAddressOf())),
"create scheduler"))
return false;
const VARIANT empty{};
if (!Check(service_->Connect(empty, empty, empty, empty), "connect"))
return false;
auto root = MakeBstr(L"\\");
return root && Check(service_->GetFolder(root.get(), folder.GetAddressOf()),
"open task folder");
}
};
LONG ReadRunEntry(const std::wstring& app_name, std::wstring* command) {
DWORD size = 0;
LONG result = RegGetValueW(HKEY_CURRENT_USER, kRunKey, app_name.c_str(),
RRF_RT_REG_SZ, nullptr, nullptr, &size);
if (result != ERROR_SUCCESS) return result;
std::vector<wchar_t> buffer(size / sizeof(wchar_t) + 1, L'\0');
result = RegGetValueW(HKEY_CURRENT_USER, kRunKey, app_name.c_str(),
RRF_RT_REG_SZ, nullptr, buffer.data(), &size);
if (result == ERROR_SUCCESS) *command = buffer.data();
return result;
}
bool RemoveRunEntry(const std::wstring& app_name) {
const LONG result =
RegDeleteKeyValueW(HKEY_CURRENT_USER, kRunKey, app_name.c_str());
if (result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND ||
result == ERROR_PATH_NOT_FOUND)
return true;
LOG_ERROR("Windows autostart remove Run entry failed: error={}", result);
return false;
}
bool EnableRunEntry(const std::wstring& app_name,
const std::wstring& executable) {
HKEY key = nullptr;
const LONG opened = RegCreateKeyExW(HKEY_CURRENT_USER, kRunKey, 0, nullptr, 0,
KEY_SET_VALUE, nullptr, &key, nullptr);
if (opened != ERROR_SUCCESS) {
LOG_ERROR("Windows autostart open Run key failed: error={}", opened);
return false;
}
const std::wstring command = L"\"" + executable + L"\"";
const LONG result = RegSetValueExW(
key, app_name.c_str(), 0, REG_SZ,
reinterpret_cast<const BYTE*>(command.c_str()),
static_cast<DWORD>((command.size() + 1) * sizeof(wchar_t)));
RegCloseKey(key);
if (result != ERROR_SUCCESS)
LOG_ERROR("Windows autostart set Run entry failed: error={}", result);
return result == ERROR_SUCCESS;
}
bool RegisterLogonTask(const std::wstring& app_name,
const std::filesystem::path& executable) {
const auto sid = CurrentUserSid();
if (sid.empty()) return false;
TaskScheduler scheduler;
if (!scheduler.Open()) return false;
auto name = MakeBstr(AutostartTaskName(app_name, sid));
auto xml = MakeBstr(BuildAutostartTaskXml(
sid, executable.wstring(), executable.parent_path().wstring()));
auto user = MakeBstr(sid);
if (!name || !xml || !user) return false;
VARIANT user_id{};
user_id.vt = VT_BSTR;
user_id.bstrVal = user.get(); // Borrowed for this synchronous call.
const VARIANT empty{};
ComPtr<IRegisteredTask> task;
return Check(scheduler.folder->RegisterTask(
name.get(), xml.get(), TASK_CREATE_OR_UPDATE, user_id, empty,
TASK_LOGON_INTERACTIVE_TOKEN, empty, task.GetAddressOf()),
"register elevated logon task");
}
bool RemoveLogonTask(const std::wstring& app_name) {
const auto sid = CurrentUserSid();
if (sid.empty()) return false;
TaskScheduler scheduler;
if (!scheduler.Open()) return false;
auto name = MakeBstr(AutostartTaskName(app_name, sid));
if (!name) return false;
const HRESULT result = scheduler.folder->DeleteTask(name.get(), 0);
return result == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND) ||
result == HRESULT_FROM_WIN32(ERROR_PATH_NOT_FOUND) ||
Check(result, "delete logon task");
}
} // namespace
std::string GetAutostartExecutablePath() {
char path[32768] = {};
const DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
return length > 0 && length < sizeof(path) ? std::string(path, length)
: std::string();
std::vector<wchar_t> path(32768);
const DWORD length =
GetModuleFileNameW(nullptr, path.data(), static_cast<DWORD>(path.size()));
if (length == 0 || length >= path.size()) return {};
return std::filesystem::path(path.data(), path.data() + length).u8string();
}
bool EnableAutostart(const std::string& app_name,
const std::string& executable_path) {
if (!std::filesystem::exists(executable_path)) return false;
HKEY key = nullptr;
if (RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_WRITE, &key) !=
ERROR_SUCCESS) {
const auto name = FromUtf8(app_name);
const auto path = FromUtf8(executable_path);
if (name.empty() || path.empty()) return false;
const std::filesystem::path executable(path);
std::error_code error;
if (!executable.is_absolute() ||
!std::filesystem::is_regular_file(executable, error))
return false;
}
std::string value = executable_path;
if (value.find(' ') != std::string::npos &&
(value.front() != '"' || value.back() != '"')) {
value = "\"" + value + "\"";
}
const LONG result = RegSetValueExA(
key, app_name.c_str(), 0, REG_SZ,
reinterpret_cast<const BYTE*>(value.c_str()),
static_cast<DWORD>(value.size() + 1));
RegCloseKey(key);
return result == ERROR_SUCCESS;
if (!kAutostartRequiresElevation) return EnableRunEntry(name, path);
// Do not remove the old entry unless registering its replacement succeeded.
if (!RegisterLogonTask(name, executable)) return false;
RemoveRunEntry(name);
return true;
}
bool DisableAutostart(const std::string& app_name) {
HKEY key = nullptr;
if (RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_WRITE, &key) !=
ERROR_SUCCESS) {
return false;
}
const LONG result = RegDeleteValueA(key, app_name.c_str());
RegCloseKey(key);
return result == ERROR_SUCCESS || result == ERROR_FILE_NOT_FOUND;
const auto name = FromUtf8(app_name);
if (name.empty()) return false;
// Portable builds must not modify an installed build's elevated task.
if (kAutostartRequiresElevation && !RemoveLogonTask(name)) return false;
return RemoveRunEntry(name);
}
bool IsAutostartEnabled(const std::string& app_name) {
HKEY key = nullptr;
if (RegOpenKeyExA(HKEY_CURRENT_USER, kRunKey, 0, KEY_READ, &key) !=
ERROR_SUCCESS) {
return false;
const auto app = FromUtf8(app_name);
if (app.empty()) return false;
if (!kAutostartRequiresElevation) {
std::wstring command;
return ReadRunEntry(app, &command) == ERROR_SUCCESS;
}
const LONG result =
RegQueryValueExA(key, app_name.c_str(), nullptr, nullptr, nullptr, nullptr);
RegCloseKey(key);
return result == ERROR_SUCCESS;
const auto sid = CurrentUserSid();
if (sid.empty()) return false;
TaskScheduler scheduler;
if (!scheduler.Open()) return false;
auto name = MakeBstr(AutostartTaskName(app, sid));
if (!name) return false;
ComPtr<IRegisteredTask> task;
if (FAILED(scheduler.folder->GetTask(name.get(), task.GetAddressOf())))
return false;
VARIANT_BOOL enabled = VARIANT_FALSE;
return SUCCEEDED(task->get_Enabled(&enabled)) && enabled == VARIANT_TRUE;
}
bool MigrateLegacyAutostart(const std::string& app_name) {
if (!kAutostartRequiresElevation) return true;
std::wstring command;
const auto name = FromUtf8(app_name);
if (name.empty()) return false;
const LONG result = ReadRunEntry(name, &command);
if (result == ERROR_FILE_NOT_FOUND || result == ERROR_PATH_NOT_FOUND)
return true;
if (result != ERROR_SUCCESS) return false;
if (command.size() >= 2 && command.front() == L'"' && command.back() == L'"')
command = command.substr(1, command.size() - 2);
const auto executable = GetAutostartExecutablePath();
if (executable.empty()) return false;
if (_wcsicmp(command.c_str(), FromUtf8(executable).c_str()) != 0) return true;
return EnableAutostart(app_name, executable);
}
bool RemoveInstalledAutostart(const std::string& app_name) {
const auto name = FromUtf8(app_name);
const auto executable = FromUtf8(GetAutostartExecutablePath());
if (name.empty() || executable.empty()) return false;
TaskScheduler scheduler;
if (!scheduler.Open()) return false;
ComPtr<IRegisteredTaskCollection> tasks;
if (!Check(scheduler.folder->GetTasks(TASK_ENUM_HIDDEN, tasks.GetAddressOf()),
"enumerate logon tasks"))
return false;
LONG count = 0;
if (!Check(tasks->get_Count(&count), "count logon tasks")) return false;
const auto prefix = name + L" Autostart ";
std::vector<std::wstring> remove;
for (LONG index = 1; index <= count; ++index) {
VARIANT item{};
item.vt = VT_I4;
item.lVal = index;
ComPtr<IRegisteredTask> task;
if (!Check(tasks->get_Item(item, task.GetAddressOf()), "read logon task"))
return false;
BSTR raw_name = nullptr;
if (!Check(task->get_Name(&raw_name), "read task name")) return false;
Bstr task_name(raw_name, SysFreeString);
const std::wstring task_text(task_name.get() ? task_name.get() : L"");
if (task_text.compare(0, prefix.size(), prefix) != 0) continue;
ComPtr<ITaskDefinition> definition;
ComPtr<IActionCollection> actions;
ComPtr<IAction> action;
ComPtr<IExecAction> exec;
LONG action_count = 0;
if (!Check(task->get_Definition(definition.GetAddressOf()),
"read task definition") ||
!Check(definition->get_Actions(actions.GetAddressOf()),
"read task actions") ||
!Check(actions->get_Count(&action_count), "count task actions"))
return false;
if (action_count != 1) continue;
if (!Check(actions->get_Item(1, action.GetAddressOf()), "read task action"))
return false;
if (FAILED(action.As(&exec))) continue;
BSTR raw_path = nullptr;
if (!Check(exec->get_Path(&raw_path), "read task executable")) return false;
Bstr path(raw_path, SysFreeString);
if (path && _wcsicmp(path.get(), executable.c_str()) == 0)
remove.push_back(task_text);
}
// Collect names first; deleting during enumeration can change its indices.
for (const auto& task : remove) {
auto task_name = MakeBstr(task);
if (!task_name || !Check(scheduler.folder->DeleteTask(task_name.get(), 0),
"remove installed logon task"))
return false;
}
std::wstring command;
const LONG result = ReadRunEntry(name, &command);
if (result == ERROR_FILE_NOT_FOUND || result == ERROR_PATH_NOT_FOUND)
return true;
if (result != ERROR_SUCCESS) return false;
if (command.size() >= 2 && command.front() == L'"' && command.back() == L'"')
command = command.substr(1, command.size() - 2);
return _wcsicmp(command.c_str(), executable.c_str()) != 0 ||
RemoveRunEntry(name);
}
} // namespace crossdesk::platform
@@ -0,0 +1,88 @@
#ifndef CROSSDESK_WINDOWS_AUTOSTART_TASK_H_
#define CROSSDESK_WINDOWS_AUTOSTART_TASK_H_
#include <string>
namespace crossdesk::platform {
// Keep this in sync with crossdesk.rc and crossdesk_portable.rc. Elevating a
// portable build manually must not change its normal login startup policy.
#if !defined(CROSSDESK_DEBUG) && \
(!defined(CROSSDESK_PORTABLE) || !CROSSDESK_PORTABLE)
inline constexpr bool kAutostartRequiresElevation = true;
#else
inline constexpr bool kAutostartRequiresElevation = false;
#endif
inline std::wstring AutostartTaskName(const std::wstring& app_name,
const std::wstring& user_sid) {
return app_name + L" Autostart " + user_sid;
}
inline std::wstring EscapeTaskXml(const std::wstring& value) {
std::wstring escaped;
for (const wchar_t character : value) {
switch (character) {
case L'&':
escaped += L"&amp;";
break;
case L'<':
escaped += L"&lt;";
break;
case L'>':
escaped += L"&gt;";
break;
case L'"':
escaped += L"&quot;";
break;
case L'\'':
escaped += L"&apos;";
break;
default:
escaped += character;
break;
}
}
return escaped;
}
inline std::wstring BuildAutostartTaskXml(const std::wstring& user_sid,
const std::wstring& executable,
const std::wstring& directory) {
const auto sid = EscapeTaskXml(user_sid);
// InteractiveToken keeps the GUI in this user's logged-in desktop and does
// not store a password. A remote desktop client must also survive battery
// operation and the scheduler's default three-day execution time limit.
return L"<Task version=\"1.3\" "
L"xmlns=\"http://schemas.microsoft.com/windows/2004/02/mit/task\">"
L"<Triggers><LogonTrigger><Enabled>true</Enabled>"
L"<ExecutionTimeLimit>PT0S</ExecutionTimeLimit><UserId>" +
sid +
L"</UserId></LogonTrigger></Triggers>"
L"<Principals><Principal id=\"CurrentUser\"><UserId>" +
sid +
L"</UserId><LogonType>InteractiveToken</LogonType>"
L"<RunLevel>HighestAvailable</RunLevel></Principal></Principals>"
L"<Settings><MultipleInstancesPolicy>IgnoreNew</"
L"MultipleInstancesPolicy>"
L"<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>"
L"<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>"
L"<AllowHardTerminate>false</AllowHardTerminate>"
L"<Enabled>true</Enabled><ExecutionTimeLimit>PT0S</ExecutionTimeLimit>"
L"</Settings><Actions Context=\"CurrentUser\"><Exec><Command>" +
EscapeTaskXml(executable) + L"</Command><WorkingDirectory>" +
EscapeTaskXml(directory) +
L"</WorkingDirectory></Exec></Actions></Task>";
}
// Called at normal startup and by the installer after an upgrade. Only migrate
// a legacy Run entry pointing to this executable, leaving other copies alone.
bool MigrateLegacyAutostart(const std::string& app_name);
// Installer cleanup: remove tasks for this executable across users, preserving
// registrations belonging to other installation/portable directories.
bool RemoveInstalledAutostart(const std::string& app_name);
} // namespace crossdesk::platform
#endif
@@ -0,0 +1,77 @@
#include "application/gui_application.h"
#include <Windows.h>
#include <SDL3/SDL.h>
#include "rd_log.h"
#include "unattended_config.h"
namespace crossdesk {
int GuiApplication::RunUnattended(void* stop_event) {
if (!stop_event || !IsLocalSystemProcess() || !IsUnattendedEnabled() ||
!PrepareUnattendedDirectory(false))
return 1;
unattended_host_ = true;
DWORD session = 0;
if (!ProcessIdToSessionId(GetCurrentProcessId(), &session) || session == 0)
return 1;
path_manager_ = std::make_unique<PathManager>("CrossDesk");
const auto root = UnattendedDataPath();
cache_path_ = root.string();
exec_log_path_ = (root / L"logs").string();
dll_log_path_ = exec_log_path_;
InitializeLogger();
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
if (config_center_->SetPrivacyScreen(false) != 0 ||
config_center_->SetSaveRemotePreviews(false) != 0)
return 1;
InitializeSettings();
// The transport and device controllers do not need a Slint window, an SDL
// video driver, a tray icon, or a logged-in user's audio/clipboard session.
if (!SDL_Init(SDL_INIT_EVENTS)) return 1;
devices_.Initialize();
ULONGLONG last_status = 0;
ULONGLONG next_peer_attempt = 0;
while (WaitForSingleObject(stop_event, 16) == WAIT_TIMEOUT) {
HandleSessionCleanup();
HandlePasswordChangeResult();
HandleCredentialRecovery();
const auto now = GetTickCount64();
if (!peer_ && now >= next_peer_attempt) {
next_peer_attempt = now + 3000;
if (CreateConnectionPeer() != 0) CloseConnectionPeer();
}
HandleConnectionStatusChange();
HandleWindowsServiceIntegration();
devices_.UpdateInteractions();
ShareLocalCursorState();
if (now - last_status >= 1000) {
last_status = now;
if (!WriteUnattendedStatus(settings_.ActiveIdentity(),
signal_connected_.load(), session)) {
LOG_WARN("Could not publish unattended host status");
}
}
// Pump native device messages without constructing an application window.
MSG message{};
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
TranslateMessage(&message);
DispatchMessageW(&message);
}
}
// Quiesce network callbacks before destroying the controllers they use.
CloseAllRemoteSessions();
WaitForSessionCleanup();
keyboard_.ReleaseAllRemotePressedKeys("unattended_host_exit");
devices_.DestroyDevices();
devices_.DestroyFactories();
privacy_.Shutdown();
WriteUnattendedStatus(settings_.ActiveIdentity(), false, session);
SDL_Quit();
return 0;
}
} // namespace crossdesk
@@ -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(), &current_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
@@ -58,6 +58,10 @@ function main(plat, portable)
end
if plat == "windows" then
for _, name in ipairs({"autostart", "windows_autostart_test"}) do
assert(contains(effective_defines(targets[name]), "CROSSDESK_PORTABLE=1") ==
(portable == "true"), name .. " has inconsistent autostart privilege policy")
end
local resources = {
crossdesk = portable == "true" and "crossdesk_portable" or "crossdesk",
crossdesk_service = "crossdesk_service",
@@ -0,0 +1,84 @@
"""Validate generated Task Scheduler XML on macOS/Linux without changing startup.
Run: python3 apps/desktop/tests/windows_autostart_task_test.py
Native registration/migration coverage lives in windows_autostart_test.cpp.
"""
import pathlib
import shutil
import subprocess
import tempfile
import unittest
import xml.etree.ElementTree as ET
ROOT = pathlib.Path(__file__).resolve().parents[3]
NAMESPACE = {"t": "http://schemas.microsoft.com/windows/2004/02/mit/task"}
class AutostartTaskTest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.temporary = tempfile.TemporaryDirectory(prefix="crossdesk-autostart-test-")
cls.addClassCleanup(cls.temporary.cleanup)
directory = pathlib.Path(cls.temporary.name)
source = directory / "fixture.cpp"
source.write_text(r'''
#include <iostream>
#include "platform/windows/autostart_task.h"
int main() {
std::cout << crossdesk::platform::kAutostartRequiresElevation << '\n';
const auto xml = crossdesk::platform::BuildAutostartTaskXml(
L"S-1-5-21-123-456-789-1001", L"C:\\程序 & Tools\\CrossDesk.exe",
L"C:\\程序 & Tools");
for (wchar_t character : xml) std::cout << static_cast<unsigned>(character) << ' ';
}
''', encoding="utf-8")
compiler = shutil.which("c++") or shutil.which("clang++")
if not compiler:
raise unittest.SkipTest("C++17 compiler is unavailable")
cls.variants = {}
for name, defines in {
"installed": [], "portable": ["-DCROSSDESK_PORTABLE=1"],
"debug": ["-DCROSSDESK_DEBUG"],
"portable_debug": ["-DCROSSDESK_PORTABLE=1", "-DCROSSDESK_DEBUG"],
}.items():
binary = directory / name
subprocess.run([compiler, "-std=c++17", "-Wall", "-Wextra", "-Werror",
"-I", str(ROOT / "apps/desktop/src"), *defines,
str(source), "-o", str(binary)], check=True)
policy, codepoints = subprocess.check_output([str(binary)], text=True).split("\n", 1)
xml = "".join(chr(int(value)) for value in codepoints.split())
cls.variants[name] = (policy, ET.fromstring(xml))
def text(self, path):
return self.variants["installed"][1].findtext(path, namespaces=NAMESPACE)
def test_privilege_policy_matches_build_type(self):
self.assertEqual({name: result[0] for name, result in self.variants.items()},
{"installed": "1", "portable": "0", "debug": "0", "portable_debug": "0"})
def test_login_targets_same_interactive_user_without_password(self):
sid = "S-1-5-21-123-456-789-1001"
self.assertEqual(self.text("t:Triggers/t:LogonTrigger/t:UserId"), sid)
self.assertEqual(self.text("t:Principals/t:Principal/t:UserId"), sid)
self.assertEqual(self.text("t:Principals/t:Principal/t:LogonType"), "InteractiveToken")
self.assertEqual(self.text("t:Principals/t:Principal/t:RunLevel"), "HighestAvailable")
self.assertEqual(self.text("t:Triggers/t:LogonTrigger/t:Enabled"), "true")
def test_command_is_not_shell_interpreted_and_unicode_round_trips(self):
self.assertEqual(self.text("t:Actions/t:Exec/t:Command"), r"C:\程序 & Tools\CrossDesk.exe")
self.assertEqual(self.text("t:Actions/t:Exec/t:WorkingDirectory"), r"C:\程序 & Tools")
self.assertIsNone(self.text("t:Actions/t:Exec/t:Arguments"))
task = self.variants["installed"][1]
self.assertEqual(task.find("t:Actions", NAMESPACE).get("Context"),
task.find("t:Principals/t:Principal", NAMESPACE).get("id"))
def test_remote_client_can_run_indefinitely_on_battery(self):
self.assertEqual(self.text("t:Triggers/t:LogonTrigger/t:ExecutionTimeLimit"), "PT0S")
self.assertEqual(self.text("t:Settings/t:DisallowStartIfOnBatteries"), "false")
self.assertEqual(self.text("t:Settings/t:StopIfGoingOnBatteries"), "false")
self.assertEqual(self.text("t:Settings/t:ExecutionTimeLimit"), "PT0S")
self.assertEqual(self.text("t:Settings/t:MultipleInstancesPolicy"), "IgnoreNew")
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,278 @@
// Run from an elevated Windows terminal (the Windows CI runner is elevated).
// Uses a unique registration name and removes it before returning.
#include <windows.h>
#include <sddl.h>
#include <taskschd.h>
#include <wrl/client.h>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <string>
#include <vector>
#include "platform/autostart_backend.h"
#include "platform/windows/autostart_task.h"
#include "rd_log.h"
namespace {
using Microsoft::WRL::ComPtr;
namespace platform = crossdesk::platform;
constexpr wchar_t kRunKey[] =
L"Software\\Microsoft\\Windows\\CurrentVersion\\Run";
bool Expect(bool condition, const char* message) {
if (!condition) std::cerr << "FAIL: " << message << '\n';
return condition;
}
std::wstring UserSid() {
HANDLE token = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) return {};
DWORD size = 0;
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
std::vector<BYTE> data(size);
const BOOL read =
GetTokenInformation(token, TokenUser, data.data(), size, &size);
CloseHandle(token);
if (!read) return {};
LPWSTR sid = nullptr;
if (!ConvertSidToStringSidW(
reinterpret_cast<TOKEN_USER*>(data.data())->User.Sid, &sid))
return {};
const std::wstring result(sid);
LocalFree(sid);
return result;
}
bool SetRun(const std::wstring& name, const std::wstring& path) {
HKEY key = nullptr;
if (RegCreateKeyExW(HKEY_CURRENT_USER, kRunKey, 0, nullptr, 0, KEY_SET_VALUE,
nullptr, &key, nullptr) != ERROR_SUCCESS)
return false;
const std::wstring command = L"\"" + path + L"\"";
const LONG result = RegSetValueExW(
key, name.c_str(), 0, REG_SZ,
reinterpret_cast<const BYTE*>(command.c_str()),
static_cast<DWORD>((command.size() + 1) * sizeof(wchar_t)));
RegCloseKey(key);
return result == ERROR_SUCCESS;
}
std::wstring ReadRun(const std::wstring& name) {
wchar_t value[32768] = {};
DWORD size = sizeof(value);
if (RegGetValueW(HKEY_CURRENT_USER, kRunKey, name.c_str(), RRF_RT_REG_SZ,
nullptr, value, &size) != ERROR_SUCCESS)
return {};
return value;
}
bool CheckTask(ITaskFolder* folder, BSTR task_name,
const std::filesystem::path& executable,
const std::wstring& sid) {
ComPtr<IRegisteredTask> task;
if (!Expect(SUCCEEDED(folder->GetTask(task_name, task.GetAddressOf())),
"task exists"))
return false;
ComPtr<ITaskDefinition> definition;
if (FAILED(task->get_Definition(definition.GetAddressOf()))) return false;
ComPtr<IPrincipal> principal;
if (FAILED(definition->get_Principal(principal.GetAddressOf()))) return false;
TASK_RUNLEVEL level = TASK_RUNLEVEL_LUA;
TASK_LOGON_TYPE logon = TASK_LOGON_NONE;
bool ok = Expect(SUCCEEDED(principal->get_RunLevel(&level)) &&
level == TASK_RUNLEVEL_HIGHEST,
"task requests elevation");
ok &= Expect(SUCCEEDED(principal->get_LogonType(&logon)) &&
logon == TASK_LOGON_INTERACTIVE_TOKEN,
"task uses interactive user");
BSTR user = nullptr;
ok &= Expect(SUCCEEDED(principal->get_UserId(&user)) && user && sid == user,
"task belongs to current user SID");
SysFreeString(user);
ComPtr<ITriggerCollection> triggers;
ComPtr<ITrigger> trigger;
ComPtr<ILogonTrigger> logon_trigger;
if (FAILED(definition->get_Triggers(triggers.GetAddressOf())) ||
FAILED(triggers->get_Item(1, trigger.GetAddressOf())) ||
FAILED(trigger.As(&logon_trigger)))
return false;
user = nullptr;
ok &=
Expect(SUCCEEDED(logon_trigger->get_UserId(&user)) && user && sid == user,
"logon trigger is restricted to the same user");
SysFreeString(user);
ComPtr<ITaskSettings> settings;
if (FAILED(definition->get_Settings(settings.GetAddressOf()))) return false;
VARIANT_BOOL value = VARIANT_TRUE;
ok &= Expect(SUCCEEDED(settings->get_DisallowStartIfOnBatteries(&value)) &&
value == VARIANT_FALSE,
"starts on battery");
ok &= Expect(SUCCEEDED(settings->get_StopIfGoingOnBatteries(&value)) &&
value == VARIANT_FALSE,
"survives switching to battery");
BSTR limit = nullptr;
ok &= Expect(SUCCEEDED(settings->get_ExecutionTimeLimit(&limit)) && limit &&
std::wstring(limit) == L"PT0S",
"has no three-day time limit");
SysFreeString(limit);
ComPtr<IActionCollection> actions;
ComPtr<IAction> action;
ComPtr<IExecAction> exec;
if (FAILED(definition->get_Actions(actions.GetAddressOf())) ||
FAILED(actions->get_Item(1, action.GetAddressOf())) ||
FAILED(action.As(&exec)))
return false;
BSTR path = nullptr;
ok &= Expect(
SUCCEEDED(exec->get_Path(&path)) && path && executable.wstring() == path,
"Unicode command round-trips");
SysFreeString(path);
path = nullptr;
ok &= Expect(SUCCEEDED(exec->get_WorkingDirectory(&path)) && path &&
executable.parent_path().wstring() == path,
"working directory is executable directory");
SysFreeString(path);
return ok;
}
int RunTests(ITaskFolder* folder) {
const std::string app =
"CrossDeskAutostartTest-" + std::to_string(GetCurrentProcessId());
const std::wstring name(app.begin(), app.end());
const auto sid = UserSid();
if (sid.empty()) return 1;
const auto task_name = platform::AutostartTaskName(name, sid);
BSTR task_bstr = SysAllocString(task_name.c_str());
if (!task_bstr) return 1;
const auto executable =
std::filesystem::u8path(platform::GetAutostartExecutablePath());
const auto temporary = std::filesystem::temp_directory_path() /
(name + L" \u81ea\u542f & spaces");
std::filesystem::create_directories(temporary);
const auto alternate = temporary / L"CrossDesk \u6d4b\u8bd5.exe";
// Registration never executes this file. Exercise XML/path handling without
// leaving another runnable test program in the login path.
std::ofstream(alternate) << "autostart registration test";
struct Cleanup {
ITaskFolder* folder;
BSTR task;
std::wstring name;
std::filesystem::path directory;
~Cleanup() {
folder->DeleteTask(task, 0);
SysFreeString(task);
RegDeleteKeyValueW(HKEY_CURRENT_USER, kRunKey, name.c_str());
std::error_code error;
std::filesystem::remove_all(directory, error);
}
} cleanup{folder, task_bstr, name, temporary};
bool ok = Expect(platform::DisableAutostart(app),
"disable absent entry is idempotent");
ok &= Expect(!platform::IsAutostartEnabled(app), "initially disabled");
ok &= Expect(platform::MigrateLegacyAutostart(app),
"migration without a legacy entry is a no-op");
ok &= Expect(!platform::IsAutostartEnabled(app),
"migration never opts user in");
ok &=
Expect(SetRun(name, alternate.wstring()), "seed another copy's startup");
ok &= Expect(platform::MigrateLegacyAutostart(app),
"ignore another copy during migration");
ok &= Expect(ReadRun(name) == L"\"" + alternate.wstring() + L"\"",
"another copy's registration is unchanged");
ok &= Expect(SetRun(name, executable.wstring()), "seed legacy Run entry");
ok &= Expect(
!platform::EnableAutostart(app, (temporary / L"missing.exe").u8string()),
"reject missing executable");
ok &= Expect(!ReadRun(name).empty(),
"failed registration preserves legacy entry");
ok &= Expect(platform::MigrateLegacyAutostart(app), "migrate legacy entry");
ok &= Expect(platform::IsAutostartEnabled(app), "enabled after migration");
if (platform::kAutostartRequiresElevation) {
ok &= Expect(ReadRun(name).empty(),
"remove legacy entry after successful migration");
ok &= CheckTask(folder, task_bstr, executable, sid);
} else {
ComPtr<IRegisteredTask> absent;
ok &= Expect(FAILED(folder->GetTask(task_bstr, absent.GetAddressOf())),
"portable/debug does not create elevated task");
ok &= Expect(!ReadRun(name).empty(),
"portable/debug retains ordinary startup");
}
ok &= Expect(platform::EnableAutostart(app, alternate.u8string()),
"update executable path");
ok &= Expect(platform::EnableAutostart(app, alternate.u8string()),
"enable is idempotent");
if (platform::kAutostartRequiresElevation) {
ok &= CheckTask(folder, task_bstr, alternate, sid);
ComPtr<IRegisteredTask> task;
if (SUCCEEDED(folder->GetTask(task_bstr, task.GetAddressOf()))) {
ok &= Expect(SUCCEEDED(task->put_Enabled(VARIANT_FALSE)),
"disable task externally");
ok &= Expect(!platform::IsAutostartEnabled(app),
"disabled task is not reported enabled");
ok &= Expect(platform::EnableAutostart(app, alternate.u8string()),
"reenable task");
} else {
ok = false;
}
} else {
ok &= Expect(ReadRun(name) == L"\"" + alternate.wstring() + L"\"",
"Run command quotes Unicode path with spaces");
}
ok &= Expect(platform::RemoveInstalledAutostart(app),
"uninstall skips another executable");
ok &= Expect(platform::IsAutostartEnabled(app),
"other executable keeps its startup");
ok &= Expect(platform::EnableAutostart(app, executable.u8string()),
"restore own executable");
ok &= Expect(platform::RemoveInstalledAutostart(app),
"uninstall removes own registration");
ok &= Expect(!platform::IsAutostartEnabled(app),
"uninstalled executable is no longer scheduled");
ok &= Expect(platform::EnableAutostart(app, executable.u8string()),
"reenable before disable test");
// Disabling must clean up a leftover legacy entry as well as the task.
ok &=
Expect(SetRun(name, executable.wstring()), "seed leftover legacy entry");
ok &=
Expect(platform::DisableAutostart(app), "disable existing registration");
ok &= Expect(ReadRun(name).empty() && !platform::IsAutostartEnabled(app),
"all active registrations removed");
ok &= Expect(platform::DisableAutostart(app), "disable twice succeeds");
return ok ? 0 : 1;
}
} // namespace
int main() {
crossdesk::InitLogger(std::filesystem::temp_directory_path().string(),
"crossdesk-autostart-test");
// Exercise the existing-STA case used by GUI settings as well as normal COM
// cleanup: the backend must not uninitialize this caller-owned apartment.
if (FAILED(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED))) return 1;
int result = 1;
{
ComPtr<ITaskService> service;
ComPtr<ITaskFolder> folder;
const VARIANT empty{};
BSTR root = SysAllocString(L"\\");
if (root &&
SUCCEEDED(CoCreateInstance(CLSID_TaskScheduler, nullptr,
CLSCTX_INPROC_SERVER,
IID_PPV_ARGS(service.GetAddressOf()))) &&
SUCCEEDED(service->Connect(empty, empty, empty, empty)) &&
SUCCEEDED(service->GetFolder(root, folder.GetAddressOf()))) {
result = RunTests(folder.Get());
}
SysFreeString(root);
}
CoUninitialize();
if (result == 0) std::cout << "Windows autostart integration tests passed\n";
return result;
}
@@ -0,0 +1,84 @@
#include <Windows.h>
#include <iostream>
#include <string>
#include <thread>
#include "service_lifetime.h"
int wmain(int argc, wchar_t** argv) {
if (argc == 4 && std::wstring(argv[1]) == L"--child") {
crossdesk::ServiceLifetimeWatcher watcher;
if (!watcher.Start(argv[2])) return 2;
HANDLE ready = OpenEventW(EVENT_MODIFY_STATE, FALSE, argv[3]);
if (!ready) return 3;
SetEvent(ready);
CloseHandle(ready);
Sleep(INFINITE); // The real watcher must terminate this child.
return 4;
}
wchar_t executable[32768]{};
if (!GetModuleFileNameW(nullptr, executable, 32768)) return 1;
for (bool abandon : {false, true}) {
const auto suffix = std::to_wstring(GetCurrentProcessId()) +
(abandon ? L"-abandon" : L"-release");
const auto name = L"Global\\CrossDeskLifetimeTest-" + suffix;
const auto ready_name = name + L"-ready";
HANDLE owned = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE exit_owner = CreateEventW(nullptr, TRUE, FALSE, nullptr);
HANDLE ready = CreateEventW(nullptr, TRUE, FALSE, ready_name.c_str());
if (!owned || !exit_owner || !ready) return 1;
HANDLE lifetime = nullptr;
std::thread owner([&] {
lifetime = CreateMutexW(nullptr, TRUE, name.c_str());
SetEvent(owned);
WaitForSingleObject(exit_owner, INFINITE);
if (!abandon && lifetime) ReleaseMutex(lifetime);
});
WaitForSingleObject(owned, INFINITE);
bool ok = lifetime != nullptr;
if (ok) {
// Cancellation during an ordinary helper exit must not wait for or
// release the service's ownership.
crossdesk::ServiceLifetimeWatcher cancellation;
ok = cancellation.Start(name);
}
std::wstring command = L"\"" + std::wstring(executable) + L"\" --child \"" +
name + L"\" \"" + ready_name + L"\"";
STARTUPINFOW startup{};
startup.cb = sizeof(startup);
PROCESS_INFORMATION child{};
if (ok)
ok = CreateProcessW(executable, command.data(), nullptr, nullptr, FALSE,
CREATE_NO_WINDOW, nullptr, nullptr, &startup,
&child) != FALSE;
if (ok)
ok = WaitForSingleObject(ready, 10000) == WAIT_OBJECT_0 &&
WaitForSingleObject(child.hProcess, 0) == WAIT_TIMEOUT;
SetEvent(exit_owner);
owner.join();
if (ok) ok = WaitForSingleObject(child.hProcess, 5000) == WAIT_OBJECT_0;
DWORD exit_code = 0;
if (ok)
ok = GetExitCodeProcess(child.hProcess, &exit_code) &&
exit_code == ERROR_BROKEN_PIPE;
if (child.hProcess) {
if (!ok) TerminateProcess(child.hProcess, 1);
WaitForSingleObject(child.hProcess, 5000);
CloseHandle(child.hProcess);
CloseHandle(child.hThread);
}
if (lifetime) CloseHandle(lifetime);
CloseHandle(owned);
CloseHandle(exit_owner);
CloseHandle(ready);
if (!ok) {
std::cerr << "Service lifetime test failed, abandoned=" << abandon
<< '\n';
return 1;
}
}
std::cout
<< "Service release, crash and watcher cancellation checks passed\n";
}
@@ -0,0 +1,37 @@
#include <cstdlib>
#include <iostream>
#include "platform/windows/service/unattended_policy.h"
int main() {
using crossdesk::UnattendedAction;
using Action = crossdesk::UnattendedProcessAction;
const auto expect = [](bool value, const char* message) {
if (!value) {
std::cerr << message << '\n';
std::exit(1);
}
};
constexpr uint32_t none = 0xFFFFFFFFu;
expect(UnattendedAction(false, 1, false, none) == Action::keep,
"fresh installs must not start a network host");
expect(UnattendedAction(true, none, false, none) == Action::keep,
"boot before a console exists must wait");
expect(UnattendedAction(true, 0, false, none) == Action::keep,
"never capture service session zero");
expect(UnattendedAction(true, 1, false, none) == Action::start,
"a pre-login console must start the host");
expect(UnattendedAction(true, 1, true, 1) == Action::keep,
"login, lock and GUI exit must retain the same host");
expect(UnattendedAction(true, 2, true, 1) == Action::stop,
"user switching must stop capture in the old console first");
expect(UnattendedAction(true, 2, false, none) == Action::start,
"a replacement console must start after the old host exits");
expect(UnattendedAction(true, none, true, 2) == Action::stop,
"a detached console must not keep sending frames");
expect(UnattendedAction(false, 2, true, 2) == Action::stop,
"disabling unattended mode must stop its host");
expect(UnattendedAction(true, 2, false, 2) == Action::start,
"a crashed host must restart even if its session is unchanged");
std::cout << "Unattended process lifecycle checks passed\n";
}
@@ -0,0 +1,107 @@
#include <Windows.h>
#include <Aclapi.h>
#include <sddl.h>
#include <filesystem>
#include <fstream>
#include <iostream>
#include "unattended_config.h"
namespace {
bool Check(bool condition, const char* message) {
if (!condition) std::cerr << message << ", Win32=" << GetLastError() << '\n';
return condition;
}
} // namespace
int main() {
using crossdesk::unattended_detail::PreparePrivateDirectory;
if (!Check(crossdesk::IsAdministratorProcess(), "Run this test elevated"))
return 1;
const auto root =
std::filesystem::temp_directory_path() /
(L"crossdesk-unattended-test-" + std::to_wstring(GetCurrentProcessId()));
struct Cleanup {
std::filesystem::path path;
~Cleanup() {
std::error_code error;
std::filesystem::remove_all(path, error);
}
} cleanup{root};
// Never reuse/remove a directory we did not create for this test.
if (std::filesystem::exists(root)) {
cleanup.path.clear();
return 1;
}
if (!Check(PreparePrivateDirectory(root, true), "create private profile") ||
!Check(PreparePrivateDirectory(root, true),
"idempotent profile validation"))
return 1;
const auto secret = root / L"credential.txt";
{
std::ofstream output(secret);
output << "test credential";
}
HANDLE token = nullptr, restricted = nullptr;
BYTE admin_sid[SECURITY_MAX_SID_SIZE];
DWORD sid_size = sizeof(admin_sid);
if (!CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, admin_sid,
&sid_size) ||
!OpenProcessToken(GetCurrentProcess(),
TOKEN_DUPLICATE | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY |
TOKEN_IMPERSONATE,
&token))
return 1;
SID_AND_ATTRIBUTES disabled{admin_sid, 0};
const bool restricted_ok =
CreateRestrictedToken(token, DISABLE_MAX_PRIVILEGE, 1, &disabled, 0,
nullptr, 0, nullptr, &restricted) != FALSE;
CloseHandle(token);
if (!Check(restricted_ok, "create unprivileged token")) return 1;
const bool impersonated = ImpersonateLoggedOnUser(restricted) != FALSE;
bool denied = false;
if (impersonated) {
HANDLE file = CreateFileW(secret.c_str(), GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, 0, nullptr);
denied =
file == INVALID_HANDLE_VALUE && GetLastError() == ERROR_ACCESS_DENIED;
if (file != INVALID_HANDLE_VALUE) CloseHandle(file);
if (!RevertToSelf()) std::terminate();
}
CloseHandle(restricted);
if (!Check(impersonated && denied,
"ordinary token must not read credentials"))
return 1;
const auto link = root / L"link";
if (!Check(CreateSymbolicLinkW(link.c_str(), root.c_str(),
SYMBOLIC_LINK_FLAG_DIRECTORY) != FALSE,
"create directory link") ||
!Check(!PreparePrivateDirectory(link, false),
"reject directory reparse points"))
return 1;
std::filesystem::remove(link);
PSECURITY_DESCRIPTOR descriptor = nullptr;
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;GR;;;WD)",
SDDL_REVISION_1, &descriptor, nullptr))
return 1;
BOOL present = FALSE, defaulted = FALSE;
PACL dacl = nullptr;
GetSecurityDescriptorDacl(descriptor, &present, &dacl, &defaulted);
const auto result = SetNamedSecurityInfoW(
const_cast<wchar_t*>(root.c_str()), SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION, nullptr,
nullptr, dacl, nullptr);
LocalFree(descriptor);
if (!Check(result == ERROR_SUCCESS, "make profile permissive") ||
!Check(!PreparePrivateDirectory(root, false),
"reject exposed credentials") ||
!Check(!PreparePrivateDirectory(root, true),
"never silently adopt an unsafe profile"))
return 1;
std::cout << "Unattended profile ACL and reparse-point checks passed\n";
}
+45 -1
View File
@@ -184,6 +184,12 @@ function setup_targets()
set_default(false)
add_files("apps/desktop/tests/windows_service_mouse_ipc_test.cpp")
target("windows_unattended_policy_test")
set_kind("binary")
set_default(false)
add_includedirs("apps/desktop/src")
add_files("apps/desktop/tests/windows_unattended_policy_test.cpp")
target("windows_mouse_controller_safety_test")
set_kind("binary")
set_default(false)
@@ -396,7 +402,11 @@ function setup_targets()
add_deps("rd_log")
add_files("apps/desktop/src/autostart/*.cpp")
if is_os("windows") then
if is_config("CROSSDESK_PORTABLE", true) then
add_defines("CROSSDESK_PORTABLE=1")
end
add_files("apps/desktop/src/platform/windows/autostart.cpp")
add_syslinks("Advapi32", "Ole32", "OleAut32", "Taskschd", {public = true})
elseif is_os("macosx") then
add_files("apps/desktop/src/platform/macos/autostart.cpp")
elseif is_os("linux") then
@@ -405,6 +415,38 @@ function setup_targets()
add_includedirs("apps/desktop/src")
add_includedirs("apps/desktop/src/autostart", {public = true})
if is_os("windows") then
target("windows_unattended_config")
set_kind("object")
add_packages("nlohmann_json")
add_files("apps/desktop/src/platform/windows/service/unattended_config.cpp")
add_includedirs("apps/desktop/src/platform/windows/service")
add_syslinks("Advapi32", "Shell32", "Ole32", {public = true})
target("windows_unattended_security_test")
set_kind("binary")
set_default(false)
add_deps("windows_unattended_config")
add_includedirs("apps/desktop/src/platform/windows/service")
add_files("apps/desktop/tests/windows_unattended_security_test.cpp")
target("windows_service_lifetime_test")
set_kind("binary")
set_default(false)
add_includedirs("apps/desktop/src/platform/windows/service")
add_files("apps/desktop/tests/windows_service_lifetime_test.cpp")
target("windows_autostart_test")
set_kind("binary")
set_default(false)
add_deps("autostart", "rd_log")
add_includedirs("apps/desktop/src")
if is_config("CROSSDESK_PORTABLE", true) then
add_defines("CROSSDESK_PORTABLE=1")
end
add_files("apps/desktop/tests/windows_autostart_test.cpp")
end
target("config_center")
set_kind("object")
add_deps("rd_log", "autostart")
@@ -476,6 +518,8 @@ function setup_targets()
add_includedirs("apps/desktop/src/platform/common/input")
if is_os("windows") then
add_cxxflags("/bigobj")
add_deps("windows_unattended_config")
add_files("apps/desktop/src/platform/windows/gui/application/unattended_application.cpp")
add_links("opengl32")
add_syslinks("Comctl32", {public = true})
add_files("apps/desktop/src/platform/windows/gui/tray/win_tray.cpp",
@@ -535,7 +579,7 @@ function setup_targets()
target("crossdesk_service")
set_kind("binary")
add_packages("nlohmann_json")
add_deps("rd_log", "path_manager")
add_deps("rd_log", "path_manager", "windows_unattended_config")
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
add_files("apps/desktop/src/platform/windows/service/main.cpp",
"apps/desktop/src/platform/windows/service/service_host.cpp")