mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-18 09:53:05 +08:00
[feat] add Windows pre-login remote control and fix autostart
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user