[fix] reconcile Windows autostart and preserve applied settings

This commit is contained in:
dijunkun
2026-11-10 22:53:32 +08:00
parent c179b3c243
commit 8370bf95d1
13 changed files with 210 additions and 96 deletions
@@ -187,16 +187,20 @@ int ConfigCenter::Load() {
return 0;
}
int ConfigCenter::CommitIni() {
int ConfigCenter::CommitIni(bool warn_only) {
// Write beside the destination and rename only after a complete save. A
// failed write must not truncate the last usable configuration.
const auto log_failure = [warn_only](const std::string& message) {
if (warn_only) LOG_WARN("{}", message);
else LOG_ERROR("{}", message);
};
static std::atomic<unsigned long> sequence{0};
const std::string temporary = config_path_ + ".tmp-" +
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) +
"-" + std::to_string(sequence++);
std::error_code error;
if (ini_->SaveFile(temporary.c_str()) < 0) {
LOG_ERROR("Failed to write configuration: {}", config_path_);
log_failure("Failed to write configuration: " + config_path_);
std::filesystem::remove(temporary, error);
return -1;
}
@@ -204,14 +208,14 @@ int ConfigCenter::CommitIni() {
if (!error && std::filesystem::is_regular_file(previous)) {
std::filesystem::permissions(temporary, previous.permissions(), error);
if (error) {
LOG_ERROR("Failed to preserve configuration permissions: {}", error.message());
log_failure("Failed to preserve configuration permissions: " + error.message());
std::filesystem::remove(temporary, error);
return -1;
}
}
std::filesystem::rename(temporary, config_path_, error);
if (!error) return 0;
LOG_ERROR("Failed to replace configuration {}: {}", config_path_, error.message());
log_failure("Failed to replace configuration " + config_path_ + ": " + error.message());
std::filesystem::remove(temporary, error);
return -1;
}
@@ -428,15 +432,26 @@ int ConfigCenter::SetSelfHosted(bool enabled) {
}
int ConfigCenter::SetAutostart(bool enabled) {
std::string error = "Operating system startup registration failed";
#ifdef _WIN32
if (!SupportsServiceAutostart()) return enabled ? -1 : 0;
const bool previous = IsServiceAutostartEnabled();
const bool update_registration = true;
const auto configure = [&error](bool value) { return SetServiceAutostart(value, error); };
// System registration determines success; INI only mirrors the last
// successfully applied preference. Unsupported builds never touch services.
if (!SupportsServiceAutostart()) {
if (enabled) return -1;
} else if (!ServiceAutostartMatches(enabled)) {
std::string error;
if (!SetServiceAutostart(enabled, error)) {
LOG_ERROR("Cannot configure autostart: {}", error);
return -1;
}
}
enable_autostart_ = enabled;
ini_->SetBoolValue(section_, "enable_autostart", enabled);
// Keep both in-memory copies even if the file is temporarily unwritable,
// so a later settings save can persist them. Never roll back the service.
CommitIni(true);
return 0;
#else
const bool previous = IsAutostartEnabled("CrossDesk");
const bool update_registration = enabled != previous;
const auto configure = [](bool value) {
return value ? EnableAutostart("CrossDesk") : DisableAutostart("CrossDesk");
};
@@ -448,18 +463,18 @@ int ConfigCenter::SetAutostart(bool enabled) {
return -1;
}
#endif
#endif
if (update_registration && !configure(enabled)) {
LOG_ERROR("Cannot configure autostart: {}", error);
if (enabled != previous && !configure(enabled)) {
LOG_ERROR("Cannot configure autostart");
return -1;
}
if (StoreValues({{"enable_autostart", enabled ? "true" : "false"}}) != 0) {
if (enabled != previous && !configure(previous))
LOG_ERROR("Cannot restore autostart: {}", error);
LOG_ERROR("Cannot restore autostart");
return -1;
}
enable_autostart_ = enabled;
return 0;
#endif
}
int ConfigCenter::SetDaemon(bool enabled) {
@@ -93,7 +93,7 @@ class ConfigCenter {
int ApplySharedHostConfig(const std::string& contents);
private:
int CommitIni();
int CommitIni(bool warn_only = false);
int StoreValues(std::initializer_list<std::pair<const char*, std::string>> values);
mutable std::mutex file_path_mutex_;
std::string config_path_;
@@ -1216,9 +1216,13 @@ int GuiApplication::Run() {
#endif
#ifdef _WIN32
// Upgrade an already-enabled standalone host before the GUI opens its peer.
// Until an identity is attached the GUI can still run on its own identity,
// e.g. before its first connection has obtained a device ID.
if (SupportsServiceAutostart() && IsUnattendedEnabled() && !shared_identity_) {
if (!PrepareSharedGuiIdentity(false) || config_center_->SetAutostart(true) != 0)
return -1;
if (!PrepareSharedGuiIdentity(false))
LOG_WARN("Could not upgrade the unattended host; boot startup stays off");
else if (config_center_->SetAutostart(true) != 0)
return -1; // Attached to a host that could not be configured.
}
#endif
#if defined(__linux__) && !defined(__OHOS__)
+39 -14
View File
@@ -251,11 +251,17 @@ bool MigrateLegacyAutostart(const std::string& app_name) {
return EnableAutostart(app_name, executable);
}
bool RemoveInstalledAutostart(const std::string& app_name) {
namespace {
bool CollectInstalledAutostart(const std::string& app_name,
TaskScheduler& scheduler,
std::vector<std::wstring>& matching_tasks,
bool& matching_run_entry) {
matching_tasks.clear();
matching_run_entry = false;
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()),
@@ -264,7 +270,6 @@ bool RemoveInstalledAutostart(const std::string& app_name) {
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;
@@ -291,19 +296,14 @@ bool RemoveInstalledAutostart(const std::string& app_name) {
if (action_count != 1) continue;
if (!Check(actions->get_Item(1, action.GetAddressOf()), "read task action"))
return false;
if (FAILED(action.As(&exec))) continue;
const HRESULT queried = action.As(&exec);
if (queried == E_NOINTERFACE) continue;
if (!Check(queried, "read executable action")) return false;
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;
matching_tasks.push_back(task_text);
}
std::wstring command;
const LONG result = ReadRunEntry(name, &command);
@@ -312,8 +312,33 @@ bool RemoveInstalledAutostart(const std::string& app_name) {
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);
matching_run_entry = _wcsicmp(command.c_str(), executable.c_str()) == 0;
return true;
}
} // namespace
bool IsInstalledAutostartAbsent(const std::string& app_name) {
TaskScheduler scheduler;
std::vector<std::wstring> tasks;
bool run_entry = false;
return CollectInstalledAutostart(app_name, scheduler, tasks, run_entry) &&
tasks.empty() && !run_entry;
}
bool RemoveInstalledAutostart(const std::string& app_name) {
TaskScheduler scheduler;
std::vector<std::wstring> tasks;
bool run_entry = false;
if (!CollectInstalledAutostart(app_name, scheduler, tasks, run_entry)) return false;
// Collect names first; deleting during enumeration can change its indices.
for (const auto& task : tasks) {
auto task_name = MakeBstr(task);
if (!task_name || !Check(scheduler.folder->DeleteTask(task_name.get(), 0),
"remove installed logon task"))
return false;
}
return !run_entry || RemoveRunEntry(FromUtf8(app_name));
}
} // namespace crossdesk::platform
@@ -83,6 +83,11 @@ bool MigrateLegacyAutostart(const std::string& app_name);
// registrations belonging to other installation/portable directories.
bool RemoveInstalledAutostart(const std::string& app_name);
// Read-only counterpart of RemoveInstalledAutostart. Includes disabled tasks
// across users and this user's legacy Run entry for the current executable.
// False means either a matching registration remains or a query failed.
bool IsInstalledAutostartAbsent(const std::string& app_name);
} // namespace crossdesk::platform
#endif
@@ -6,64 +6,30 @@
#include "config_center.h"
#include "path_manager.h"
#include "platform/autostart_backend.h"
#include "platform/single_instance.h"
#include "platform/windows/autostart_task.h"
#include "platform/windows/executable_path.h"
#include "platform/windows/virtual_display/usbmmidd_virtual_display.h"
#include "platform/autostart_backend.h"
#include "rd_log.h"
#include "service_host.h"
#include "shared_identity.h"
#include "rd_log.h"
#include "unattended_config.h"
namespace crossdesk {
namespace {
struct ServiceHandle {
SC_HANDLE value;
~ServiceHandle() { if (value) CloseServiceHandle(value); }
};
bool ReadServiceStartType(DWORD& start_type) {
start_type = SERVICE_DEMAND_START;
ServiceHandle manager{OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT)};
if (!manager.value) return false;
ServiceHandle service{OpenServiceW(manager.value, L"CrossDeskService", SERVICE_QUERY_CONFIG)};
const DWORD open_error = service.value ? ERROR_SUCCESS : GetLastError();
bool read = open_error == ERROR_SERVICE_DOES_NOT_EXIST;
if (service.value) {
alignas(QUERY_SERVICE_CONFIGW) BYTE buffer[8192]{};
auto* config = reinterpret_cast<QUERY_SERVICE_CONFIGW*>(buffer);
DWORD size = 0;
read = QueryServiceConfigW(service.value, config, sizeof(buffer), &size) != FALSE;
if (read) start_type = config->dwStartType;
}
return read;
}
bool ConfigureServiceAutostart(bool enabled) {
ServiceHandle manager{OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT)};
if (!manager.value) return false;
ServiceHandle service{OpenServiceW(manager.value, L"CrossDeskService", SERVICE_CHANGE_CONFIG)};
if (!service.value) return !enabled && GetLastError() == ERROR_SERVICE_DOES_NOT_EXIST;
return ChangeServiceConfigW(service.value, SERVICE_NO_CHANGE,
enabled ? SERVICE_AUTO_START : SERVICE_DEMAND_START, SERVICE_NO_CHANGE,
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr) != FALSE;
}
} // namespace
bool SupportsServiceAutostart() {
return platform::kAutostartRequiresElevation;
}
bool IsServiceAutostartEnabled() {
if (!SupportsServiceAutostart() || !IsUnattendedEnabled() ||
!IsSharedIdentityServiceConfigured() || !SharedHostIdentity::Configured()) return false;
DWORD start_type = 0;
return ReadServiceStartType(start_type) && start_type == SERVICE_AUTO_START;
}
bool CleanupLegacyServiceAutostart() {
return !SupportsServiceAutostart() || !IsServiceAutostartEnabled() ||
platform::MigrateLegacyAutostart("CrossDesk");
if (!SupportsServiceAutostart()) return true;
// Enabling sets the flag and disabling removes every task, so a task without
// the flag predates service startup. Remove it rather than silently turning
// it into pre-login access. With the flag set, only fold a Run entry back
// into the elevated handoff task.
return IsUnattendedEnabled() ? platform::MigrateLegacyAutostart("CrossDesk")
: platform::RemoveInstalledAutostart("CrossDesk");
}
bool SetServiceAutostart(bool enabled, std::string& error) {
@@ -84,22 +50,17 @@ bool SetServiceAutostart(bool enabled, std::string& error) {
if (error.empty()) error = "Another service configuration is in progress";
return false;
}
DWORD start_type = 0;
if (!ReadServiceStartType(start_type)) {
error = "Could not query Windows service startup configuration";
return false;
}
if (!enabled) {
// Disarm the host without stopping the lock-screen service used by GUI
// sessions. A missing service/binary must not prevent disabling startup.
if (SetUnattendedEnabled(false) && ConfigureServiceAutostart(false) &&
if (SetUnattendedEnabled(false) && SetCrossDeskServiceAutoStart(false) &&
platform::RemoveInstalledAutostart("CrossDesk")) return true;
error = "Could not disable unattended startup";
return false;
}
const auto executable = platform::GetAutostartExecutablePath();
const auto service = std::filesystem::u8path(executable).parent_path() / L"crossdesk_service.exe";
const auto service = platform::SiblingExecutablePath(L"crossdesk_service.exe");
std::error_code file_error;
if (executable.empty() || !std::filesystem::is_regular_file(service, file_error) ||
!std::filesystem::is_regular_file(service.parent_path() / L"crossdesk_session_helper.exe", file_error)) {
@@ -113,9 +74,17 @@ bool SetServiceAutostart(bool enabled, std::string& error) {
error = "Enable boot startup from the running GUI settings, or close the GUI first";
return false;
}
DWORD start_type = SERVICE_DEMAND_START;
const auto registration =
QueryCrossDeskServiceRegistration(service.wstring(), nullptr, &start_type);
if (registration == ServiceRegistrationState::query_failed) {
error = "Could not query Windows service startup configuration";
return false;
}
const bool current = IsSharedIdentityServiceConfigured() &&
QueryCrossDeskServiceRegistration(service.wstring()) == ServiceRegistrationState::current;
if (!current && IsCrossDeskServiceInstalled() && !StopCrossDeskService(20000)) {
registration == ServiceRegistrationState::current;
if (!current && registration != ServiceRegistrationState::not_installed &&
!StopCrossDeskService(20000)) {
error = "Could not stop the old service to configure shared identity";
return false;
}
@@ -130,7 +99,7 @@ bool SetServiceAutostart(bool enabled, std::string& error) {
StartCrossDeskService() && platform::EnableAutostart("CrossDesk", executable)) return true;
const bool restored_flag = SetUnattendedEnabled(was_enabled);
const bool restored_start = ConfigureServiceAutostart(start_type == SERVICE_AUTO_START);
const bool restored_start = SetCrossDeskServiceAutoStart(start_type == SERVICE_AUTO_START);
if (!restored_flag || !restored_start)
LOG_ERROR("Could not restore the previous unattended startup configuration");
error = "Could not enable persistent host startup and the user-session GUI";
@@ -13,15 +13,21 @@ namespace crossdesk {
// Windows startup preferences control the machine host and its GUI handoff.
// Portable and Debug builds keep the existing unattended-mode restriction.
// Evaluate in the application target: callers such as config_center do not
// carry the application's CROSSDESK_PORTABLE define.
bool SupportsServiceAutostart();
bool IsServiceAutostartEnabled();
// True when the registration already matches `enabled`, so saving settings
// does not stop, reinstall or re-register anything.
bool ServiceAutostartMatches(bool enabled);
// Shared GUI/CLI entry point; serializes service changes and preserves a
// running host when the installed service already matches this executable.
bool SetServiceAutostart(bool enabled, std::string& error);
// Keep a legacy Run entry from bypassing the elevated GUI handoff task.
// Logon startup only exists as the GUI handoff of an enabled machine host.
// Remove registrations left by versions before the service-based startup.
bool CleanupLegacyServiceAutostart();
} // namespace crossdesk
#endif
#endif
@@ -0,0 +1,53 @@
#include "platform/windows/service/autostart_control.h"
#include "platform/autostart_backend.h"
#include "platform/windows/autostart_task.h"
#include "platform/windows/executable_path.h"
#include "service_host.h"
#include "shared_identity.h"
#include "unattended_config.h"
namespace crossdesk {
namespace {
std::wstring ServiceExecutable() {
return platform::SiblingExecutablePath(L"crossdesk_service.exe").wstring();
}
bool SharedHostArmed() {
return SupportsServiceAutostart() && IsUnattendedEnabled() &&
IsSharedIdentityServiceConfigured() && SharedHostIdentity::Configured();
}
} // namespace
bool IsServiceAutostartEnabled() {
DWORD start_type = SERVICE_DEMAND_START;
return SharedHostArmed() &&
QueryCrossDeskServiceRegistration(ServiceExecutable(), nullptr, &start_type) !=
ServiceRegistrationState::query_failed &&
start_type == SERVICE_AUTO_START;
}
bool ServiceAutostartMatches(bool enabled) {
DWORD start_type = SERVICE_DEMAND_START;
if (!enabled) {
bool unattended = false;
if (!QueryUnattendedEnabled(unattended) || unattended) return false;
const auto registration =
QueryCrossDeskServiceRegistration(ServiceExecutable(), nullptr, &start_type);
if (registration == ServiceRegistrationState::query_failed) return false;
if (registration != ServiceRegistrationState::not_installed &&
start_type != SERVICE_DEMAND_START && start_type != SERVICE_DISABLED)
return false;
// The flag can already be off after a partially failed disable. Only skip
// the retry after checking the same registrations that cleanup removes.
return platform::IsInstalledAutostartAbsent("CrossDesk");
}
return SharedHostArmed() &&
QueryCrossDeskServiceRegistration(ServiceExecutable(), nullptr, &start_type) ==
ServiceRegistrationState::current &&
start_type == SERVICE_AUTO_START && platform::IsAutostartEnabled("CrossDesk");
}
} // namespace crossdesk
@@ -2644,8 +2644,9 @@ std::string CrossDeskServiceHost::SendSecureDesktopTextInput(
}
ServiceRegistrationState QueryCrossDeskServiceRegistration(
const std::wstring& binary_path, DWORD* error_code) {
const std::wstring& binary_path, DWORD* error_code, DWORD* start_type) {
if (error_code) *error_code = ERROR_SUCCESS;
if (start_type) *start_type = SERVICE_DEMAND_START;
const auto failed = [error_code](DWORD error) {
if (error_code) *error_code = error;
return ServiceRegistrationState::query_failed;
@@ -2672,6 +2673,7 @@ ServiceRegistrationState QueryCrossDeskServiceRegistration(
const DWORD query_error = queried ? ERROR_SUCCESS : GetLastError();
CloseServiceHandle(service);
if (!queried) return failed(query_error);
if (start_type) *start_type = config->dwStartType;
if (!config->lpBinaryPathName || binary_path.empty() ||
config->dwStartType == SERVICE_DISABLED) {
@@ -2782,6 +2784,23 @@ bool InstallCrossDeskService(const std::wstring& binary_path) {
return true;
}
bool SetCrossDeskServiceAutoStart(bool enabled) {
SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT);
if (!manager) return false;
SC_HANDLE service =
OpenServiceW(manager, kCrossDeskServiceName, SERVICE_CHANGE_CONFIG);
const DWORD open_error = service ? ERROR_SUCCESS : GetLastError();
CloseServiceHandle(manager);
if (!service) return !enabled && open_error == ERROR_SERVICE_DOES_NOT_EXIST;
const bool changed =
ChangeServiceConfigW(service, SERVICE_NO_CHANGE,
enabled ? SERVICE_AUTO_START : SERVICE_DEMAND_START,
SERVICE_NO_CHANGE, nullptr, nullptr, nullptr,
nullptr, nullptr, nullptr, nullptr) != FALSE;
CloseServiceHandle(service);
return changed;
}
bool IsCrossDeskServiceInstalled() {
SC_HANDLE manager = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_CONNECT);
if (manager == nullptr) {
@@ -192,9 +192,15 @@ enum class ServiceRegistrationState {
repair_required,
query_failed,
};
// start_type receives the registered start type, or SERVICE_DEMAND_START when
// the service is not installed.
ServiceRegistrationState QueryCrossDeskServiceRegistration(
const std::wstring& binary_path, DWORD* error_code = nullptr);
const std::wstring& binary_path, DWORD* error_code = nullptr,
DWORD* start_type = nullptr);
bool InstallCrossDeskService(const std::wstring& binary_path);
// Switch between boot and on-demand start; a missing service counts as
// disabled.
bool SetCrossDeskServiceAutoStart(bool enabled);
bool RepairCrossDeskService(const std::wstring& binary_path);
bool UninstallCrossDeskService();
bool StartCrossDeskService();
@@ -65,13 +65,21 @@ std::filesystem::path UnattendedDataPath() {
return path;
}
bool QueryUnattendedEnabled(bool& enabled) {
enabled = false;
DWORD value = 0;
DWORD size = sizeof(value);
const LONG result = RegGetValueW(HKEY_LOCAL_MACHINE, kRegistryKey, L"Enabled",
RRF_RT_REG_DWORD | RRF_SUBKEY_WOW6464KEY, nullptr, &value, &size);
if (result == ERROR_FILE_NOT_FOUND || result == ERROR_PATH_NOT_FOUND) return true;
if (result != ERROR_SUCCESS) return false;
enabled = value == 1;
return true;
}
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 enabled = false;
return QueryUnattendedEnabled(enabled) && enabled;
}
bool SetUnattendedEnabled(bool enabled) {
@@ -9,6 +9,9 @@ namespace crossdesk {
// This directory is deliberately separate from every user's GUI profile.
std::filesystem::path UnattendedDataPath();
// Missing configuration is a successful disabled result; other read failures
// must not be mistaken for a completed disable operation.
bool QueryUnattendedEnabled(bool& enabled);
bool IsUnattendedEnabled();
bool IsSharedIdentityServiceConfigured();
bool SetUnattendedEnabled(bool enabled);