[feat] unify Windows unattended hosting and device identity

This commit is contained in:
dijunkun
2026-11-10 10:43:43 +08:00
parent 930ccd9ea1
commit 4e2532f33c
67 changed files with 3491 additions and 941 deletions
@@ -62,6 +62,9 @@
#include "platform/windows/gui/shutdown_session_guard.h"
#include "platform/windows/gui/tray/win_tray.h"
#include "platform/windows/gui/window_icons.h"
#include "platform/windows/service/autostart_control.h"
#include "platform/windows/service/interactive_desktop.h"
#include "platform/windows/service/unattended_config.h"
#elif defined(__APPLE__)
#include "platform/macos/gui/tray/mac_tray.h"
#include "platform/window_drag.h"
@@ -1172,6 +1175,19 @@ int GuiApplication::Run() {
announcements_.SetStorageDirectory(path_manager_->GetCachePath() /
"announcement_reads");
config_center_ = std::make_unique<ConfigCenter>(cache_path_ + "/config.ini");
#ifdef _WIN32
if (SupportsServiceAutostart() && SharedHostIdentity::Configured()) {
// User-session features attach after unlock; the persistent host keeps
// its transport, capture and input across this transition.
if (IsUnattendedEnabled() && !IsCurrentSessionUserDesktopActive()) {
LOG_INFO("Waiting for the unlocked user desktop before attaching GUI features");
do {
Sleep(250);
} while (IsUnattendedEnabled() && !IsCurrentSessionUserDesktopActive());
}
if (!PrepareSharedGuiIdentity(true)) return -1;
}
#endif
InitializeTrustedDevices(path_manager_->GetConfigPath() / "trusted-devices");
#ifdef __APPLE__
StartMacPermissionMigration(false);
@@ -1198,6 +1214,13 @@ int GuiApplication::Run() {
#else
InitializeSettings();
#endif
#ifdef _WIN32
// Upgrade an already-enabled standalone host before the GUI opens its peer.
if (SupportsServiceAutostart() && IsUnattendedEnabled() && !shared_identity_) {
if (!PrepareSharedGuiIdentity(false) || config_center_->SetAutostart(true) != 0)
return -1;
}
#endif
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().BindSettings(
config_center_.get(), [this] {
@@ -1572,6 +1595,10 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
}
main->set_force_relay_enabled(config_center_->IsForceRelay());
main->set_self_hosted_enabled(enable_self_hosted_);
#ifdef _WIN32
// The machine setting can also be changed by the CLI or another admin.
enable_autostart_ = config_center_->IsEnableAutostart();
#endif
main->set_autostart_enabled(enable_autostart_);
#if defined(__OHOS__)
main->set_autostart_system_managed(true);
@@ -1584,11 +1611,6 @@ void GuiApplication::ResetSettingsUi(bool gallery_window) {
main->set_privacy_local_unlock_allowed(config_center_->IsPrivacyLocalUnlockAllowed());
main->set_lock_on_disconnect_enabled(config_center_->IsLockOnDisconnect());
main->set_trusted_devices_only(config_center_->IsTrustedDevicesOnly());
#ifdef _WIN32
main->set_capture_method_visible(true);
main->set_capture_method_index(
static_cast<int>(config_center_->GetScreenCaptureMethod()));
#endif
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__))
main->set_privacy_setting_available(true);
main->set_lock_on_disconnect_available(true);
@@ -2498,6 +2520,9 @@ void GuiApplication::Tick() {
if (ShutdownDrained()) slint::quit_event_loop();
return;
}
#ifdef _WIN32
TickLocalHost();
#endif
TickLan();
TickControllerTrust();
if (LanAvailable()) {
@@ -2811,6 +2836,7 @@ void GuiApplication::ShareLocalCursorState() {
std::shared_lock lock(connection_status_mutex_);
connected_controllers.reserve(connection_status_.size());
for (const auto& [remote_id, status] : connection_status_) {
if (managed_host_frontend_ && remote_id.rfind("lan/", 0) != 0) continue;
if (status == MiniRtcConnectionStatus::MiniRtcConnectionConnected) {
connected_controllers.push_back(remote_id);
}
@@ -2953,7 +2979,7 @@ void GuiApplication::ShareLocalCursorState() {
if (!delivery.has_sent ||
delivery.last_sent.render_mode != outgoing.render_mode ||
delivery.last_sent.hidden_reason != outgoing.hidden_reason) {
LOG_DEBUG("Send cursor presentation to [{}]: mode={}, reason={}",
LOG_INFO("Send cursor presentation to [{}]: mode={}, reason={}",
recipient.remote_id, static_cast<int>(outgoing.render_mode),
static_cast<int>(outgoing.hidden_reason));
}
@@ -2967,6 +2993,18 @@ void GuiApplication::ShareLocalCursorState() {
}
bool GuiApplication::RequestPasswordChange(const std::string &password) {
#ifdef _WIN32
if (managed_host_frontend_) {
if (password.size() != 6 || password.find_first_not_of(
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") != std::string::npos)
return false;
if (!SendLocalHostCommand({{"op", "password"}, {"password", password}})) {
offline_warning_text_ = localization::signal_disconnected[localization_language_index_];
show_offline_warning_window_ = true;
}
return true;
}
#endif
const auto notify = [](const std::string& message) {
#if defined(__linux__) && !defined(__OHOS__)
HeadlessConsole::Instance().NotifyKey(message.c_str());
@@ -3035,7 +3073,7 @@ bool GuiApplication::RequestPasswordChange(const std::string &password) {
{"request_id", request_id},
{"new_password", password}};
const std::string message = request.dump();
if (MiniRtcSendSignalMessage(peer_, message.data(), message.size()) != 0) {
if (PeerSendSignalMessage(peer_, message.data(), message.size()) != 0) {
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
password_change_pending_ = false;
@@ -3056,6 +3094,7 @@ bool GuiApplication::RequestPasswordChange(const std::string &password) {
}
void GuiApplication::HandlePasswordChangeResult() {
if (managed_host_frontend_) return;
bool succeeded = false;
bool uncertain = false;
std::string error;
@@ -3129,6 +3168,7 @@ void GuiApplication::HandlePasswordChangeResult() {
}
void GuiApplication::HandleCredentialRecovery() {
if (managed_host_frontend_) return;
CredentialRecoveryAction action;
{
std::lock_guard<std::mutex> lock(password_change_mutex_);
@@ -3166,7 +3206,7 @@ void GuiApplication::HandleCredentialRecovery() {
}
const auto message = nlohmann::json{{"type", "change_password"},
{"request_id", request_id}, {"new_password", password}}.dump();
if (MiniRtcSendSignalMessage(peer_, message.data(), message.size()) != 0) {
if (PeerSendSignalMessage(peer_, message.data(), message.size()) != 0) {
std::lock_guard lock(password_change_mutex_);
password_change_result_ready_ = true;
password_change_result_uncertain_ = true;
@@ -3221,6 +3261,14 @@ void GuiApplication::UpdateLocalization() {
static std::vector<std::wstring> shutdown_reasons(localization::GetSupportedLanguages().size());
ui_->shutdown_reason = localization::GetWindowsLocalizedLabel(
language, "shutdown_reason", L"Remote session active.", shutdown_reasons);
const bool available = SupportsServiceAutostart();
for (const auto& main : {ui_->main, ui_->wall}) {
main->set_service_autostart_platform(true);
main->set_autostart_available(available);
main->set_service_autostart_hint(UiText(localization::Literal(available
? "开机后无需登录即可连接;登录后继续使用同一 ID、密码和完整功能。"
: "开机自启需要安装版 Release;便携版和 Debug 不支持无人值守服务。", language)));
}
#endif
ui_localization::ApplyMainWindowStrings(ui_->main, language);
ui_localization::ApplyStreamWindowStrings(ui_->main, language);
@@ -3242,7 +3290,7 @@ void GuiApplication::SyncAnnouncements() {
const auto request = announcements_.TakePendingRequest();
if (!request.is_null()) {
const auto payload = request.dump();
if (!peer_ || MiniRtcSendSignalMessage(peer_, payload.data(), payload.size()) != 0)
if (!peer_ || PeerSendSignalMessage(peer_, payload.data(), payload.size()) != 0)
announcements_.Failed(request["request_id"]);
}
if (ui_->announcement_version == announcements_.Version()) return;
@@ -4400,13 +4448,12 @@ void GuiApplication::BindTrustedDeviceCallbacks(bool gallery_window) {
}
// The Add action approves the current input. Import only updates the
// list; a live connection must still complete its own authentication.
if (trusted_devices_->Contains(device->public_key)) {
main->set_trusted_devices_notice(message("已在可信列表中"));
} else {
trusted_devices_->Add(*device);
main->set_trusted_devices_notice(message("已加入可信设备。"));
}
main->set_trust_pairing_input("");
const bool existing = trusted_devices_->Contains(device->public_key);
if (existing ? PublishSharedIdentity() : AddTrustedDevice(*device)) {
main->set_trusted_devices_notice(message(existing
? "已在可信列表中" : "已加入可信设备。"));
main->set_trust_pairing_input("");
} else set_error("无法更新可信设备,请检查列表容量和存储权限后重试。");
} else if ((action == "add" && !TrustController(std::string(id), std::string(key))) ||
(action == "remove" && !RemoveTrustedDevice(std::string(key)))) {
set_error("无法更新可信设备,请检查列表容量和存储权限后重试。");
@@ -4859,6 +4906,12 @@ bool GuiApplication::ValidateSelfHostedSettingsFromUi(bool gallery_window) {
}
bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
#ifdef _WIN32
// A later setter can fail after earlier values were persisted. Reconcile
// with the host on the next status even when no config request was sent.
if (managed_host_frontend_)
local_host_status_.config_dirty = local_host_status_.identity_dirty = true;
#endif
const auto previous_server = GetSignalServerSettings(*config_center_);
const bool previous_ip_enabled = config_center_->IsIpDirectEnabled();
const auto previous_connection_mode = config_center_->GetConnectionMode();
@@ -4983,11 +5036,19 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
config_center_->SetForceRelay(main->get_force_relay_enabled());
main->set_force_relay_enabled(config_center_->IsForceRelay());
#if !defined(__OHOS__)
if (config_center_->SetAutostart(enable_autostart_) != 0) {
bool identity_ready = true;
#ifdef _WIN32
identity_ready = !enable_autostart_ || PrepareSharedGuiIdentity(false);
#endif
if (!identity_ready || config_center_->SetAutostart(enable_autostart_) != 0) {
enable_autostart_ = config_center_->IsEnableAutostart();
main->set_autostart_enabled(enable_autostart_);
main->set_settings_save_error(UiText(
#ifdef _WIN32
localization::Literal("无法配置无人值守服务,请检查管理员权限和安装文件。", localization_language_index_)));
#else
localization::Literal("保存失败,请重试", localization_language_index_)));
#endif
return false;
}
#endif
@@ -5005,25 +5066,6 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
LOG_ERROR("Failed to clear saved remote previews");
}
reload_recent_connections_ = true;
#ifdef _WIN32
if (!HasActiveSession()) {
const auto previous_capture_method = config_center_->GetScreenCaptureMethod();
const auto capture_method = static_cast<ScreenCaptureMethod>(
std::clamp(main->get_capture_method_index(), 0, 3));
if (config_center_->SetScreenCaptureMethod(capture_method) != 0) {
LOG_ERROR("Failed to save screen capture method");
} else if (capture_method != previous_capture_method) {
// Reconfigure only capture; its backend does not own signaling.
devices_.StopScreenCapturer();
screen_capturer_is_started_ = false;
if (devices_.InitializeScreenCapturer() != 0) {
LOG_ERROR("Failed to apply screen capture method");
}
}
}
main->set_capture_method_index(
static_cast<int>(config_center_->GetScreenCaptureMethod()));
#endif
#if defined(_WIN32) || defined(__APPLE__) || (defined(__linux__) && !defined(__OHOS__))
if (config_center_->SetPrivacyScreen(main->get_privacy_on_connect_enabled()) !=
0) {
@@ -5053,6 +5095,11 @@ bool GuiApplication::SaveSettingsFromUi(bool gallery_window) {
return false;
}
TickControllerTrust();
if (!PublishSharedIdentity(true)) {
main->set_settings_save_error(UiText(localization::Literal(
"保存失败,请重试", localization_language_index_)));
return false;
}
ApplySettingsFromConfig(previous_server !=
GetSignalServerSettings(*config_center_));
ResetSettingsUi(gallery_window);
@@ -5107,7 +5154,7 @@ void GuiApplication::ApplySettingsFromConfig(bool reconnect) {
// Update the listening peer for incoming connections and the template
// copied by ConnectTo for outgoing connections. Existing sessions retain
// the codec/ICE snapshot with which they negotiated.
if (!peer_ || MiniRtcUpdateConnectionSettings(peer_, true, av1, turn) == 0) {
if (!peer_ || PeerUpdateConnectionSettings(peer_, true, av1, turn) == 0) {
params_.av1_encoding = av1;
params_.turn_mode = turn;
} else {
@@ -58,6 +58,9 @@ private:
void BindStreamCallbacks();
void BindServerCallbacks();
void Tick();
#ifdef _WIN32
void TickLocalHost();
#endif
void PollUpdateCheck();
void ShareLocalCursorState();
bool RequestPasswordChange(const std::string &password);
@@ -539,6 +539,9 @@ inline constexpr TranslationRow kTranslationRows[] = {
{"服务已修复并启动", {"服务已修复并启动", "Service repaired and started", "Служба восстановлена и запущена", "服務已修復並啟動", "サービスを修復し、起動しました", "서비스를 복구하고 시작했습니다", "Dienst repariert und gestartet", "Service réparé et démarré", "Servicio reparado e iniciado", "Serviço reparado e iniciado"}},
{"修复失败,请检查管理员权限和便携版文件后重试", {"修复失败,请检查管理员权限和便携版文件后重试", "Repair failed. Check administrator permission and portable package files, then retry.", "Восстановление не удалось. Проверьте права администратора и файлы портативной версии, затем повторите попытку.", "修復失敗,請檢查管理員權限和可攜版檔案後重試", "修復に失敗しました。管理者権限とポータブル版のファイルを確認して再試行してください。", "복구하지 못했습니다. 관리자 권한과 포터블 버전 파일을 확인한 후 다시 시도하세요.", "Reparatur fehlgeschlagen. Prüfen Sie die Administratorrechte und die Dateien der portablen Version und versuchen Sie es erneut.", "Échec de la réparation. Vérifiez les droits administrateur et les fichiers de la version portable, puis réessayez.", "Error al reparar. Compruebe los permisos de administrador y los archivos de la versión portátil e inténtelo de nuevo.", "Falha no reparo. Verifique a permissão de administrador e os arquivos da versão portátil e tente novamente."}},
{"远端 Windows 服务不可用,锁屏时无法操作。请在远端安装或修复 CrossDesk 服务。", {"远端 Windows 服务不可用,锁屏时无法操作。请在远端安装或修复 CrossDesk 服务。", "The remote Windows service is unavailable. Lock-screen control will not work. Install or repair the CrossDesk service on the remote device.", "Служба Windows на удалённом устройстве недоступна. Управление экраном блокировки не работает. Установите или восстановите службу CrossDesk на удалённом устройстве.", "遠端 Windows 服務無法使用,鎖定畫面時無法操作。請在遠端安裝或修復 CrossDesk 服務。", "リモートの Windows サービスが利用できないため、ロック画面を操作できません。リモート端末で CrossDesk サービスをインストールまたは修復してください。", "원격 Windows 서비스를 사용할 수 없어 잠금 화면을 제어할 수 없습니다. 원격 장치에서 CrossDesk 서비스를 설치하거나 복구하세요.", "Der Windows-Dienst auf dem entfernten Gerät ist nicht verfügbar. Der Sperrbildschirm kann nicht gesteuert werden. Installieren oder reparieren Sie dort den CrossDesk-Dienst.", "Le service Windows distant est indisponible. Le contrôle de l’écran verrouillé ne fonctionnera pas. Installez ou réparez le service CrossDesk sur l’appareil distant.", "El servicio de Windows remoto no está disponible. No se podrá controlar la pantalla de bloqueo. Instale o repare el servicio CrossDesk en el dispositivo remoto.", "O serviço do Windows remoto está indisponível. Não será possível controlar a tela de bloqueio. Instale ou repare o serviço CrossDesk no dispositivo remoto."}},
{"开机自启需要安装版 Release;便携版和 Debug 不支持无人值守服务。", {"开机自启需要安装版 Release;便携版和 Debug 不支持无人值守服务。", "Boot startup requires the installed Release build. Portable and Debug builds do not support the unattended service.", "Автозапуск требует установленной сборки Release. Переносная версия и Debug не поддерживают автономную службу.", "開機自啟需要安裝版 Release;可攜版和 Debug 不支援無人值守服務。", "自動起動にはインストール版 Release が必要です。ポータブル版と Debug は無人アクセスサービスに対応していません。", "자동 시작에는 설치형 Release 버전이 필요합니다. 포터블 및 Debug 버전은 무인 접속 서비스를 지원하지 않습니다.", "Der Systemstart erfordert die installierte Release-Version. Portable- und Debug-Versionen unterstützen den unbeaufsichtigten Dienst nicht.", "Le démarrage automatique nécessite la version Release installée. Les versions portable et Debug ne prennent pas en charge le service sans surveillance.", "El inicio automático requiere la versión Release instalada. Las versiones portátil y Debug no admiten el servicio desatendido.", "A inicialização automática exige a versão Release instalada. As versões portátil e Debug não oferecem suporte ao serviço não assistido."}},
{"无法配置无人值守服务,请检查管理员权限和安装文件。", {"无法配置无人值守服务,请检查管理员权限和安装文件。", "Could not configure the unattended service. Check administrator privileges and installation files.", "Не удалось настроить автономную службу. Проверьте права администратора и файлы установки.", "無法設定無人值守服務,請檢查管理員權限和安裝檔案。", "無人アクセスサービスを設定できません。管理者権限とインストールファイルを確認してください。", "무인 접속 서비스를 설정할 수 없습니다. 관리자 권한과 설치 파일을 확인하세요.", "Der unbeaufsichtigte Dienst konnte nicht eingerichtet werden. Prüfen Sie Administratorrechte und Installationsdateien.", "Impossible de configurer le service sans surveillance. Vérifiez les droits administrateur et les fichiers d’installation.", "No se pudo configurar el servicio desatendido. Compruebe los permisos de administrador y los archivos de instalación.", "Não foi possível configurar o serviço não assistido. Verifique as permissões de administrador e os arquivos de instalação."}},
{"开机后无需登录即可连接;登录后继续使用同一 ID、密码和完整功能。", {"开机后无需登录即可连接;登录后继续使用同一 ID、密码和完整功能。", "Connect after boot without signing in. After login, keep the same ID, password and full functionality.", "Подключайтесь после загрузки без входа в Windows. После входа сохраняются ID, пароль и все функции.", "開機後無需登入即可連線;登入後繼續使用同一 ID、密碼和完整功能。", "サインイン前でも接続できます。サインイン後も同じ ID とパスワードですべての機能を利用できます。", "부팅 후 로그인하지 않아도 연결할 수 있습니다. 로그인 후에도 같은 ID와 비밀번호로 모든 기능을 사용할 수 있습니다.", "Verbinden Sie sich nach dem Start ohne Anmeldung. Nach der Anmeldung bleiben ID, Passwort und alle Funktionen erhalten.", "Connectez-vous après le démarrage sans ouvrir de session. Après connexion, conservez les mêmes ID et mot de passe et toutes les fonctions.", "Conéctese tras arrancar sin iniciar sesión. Después de iniciar sesión, conserve el mismo ID, contraseña y todas las funciones.", "Conecte-se após a inicialização sem fazer login. Após o login, mantenha o mesmo ID, senha e todas as funções."}},
};
struct NamedTranslation { const char* name; const char* literal; };
@@ -681,8 +684,6 @@ inline constexpr NamedTranslation kNamedTranslations[] = {
{"remote_unlock_requires_secure_desktop", "当前仍需要安全桌面专用采集/输入"},
{"settings", "设置"},
{"language", "语言"},
{"screen_capture_method", "采集方式"},
{"screen_capture_method_auto", "自动"},
{"video_settings", "画面设置"},
{"remote_update_title", "建议升级被控端"},
{"remote_update", "新版本包含功能改进和性能优化,建议将被控端升级至最新版本。"},
@@ -1195,8 +1196,6 @@ inline constexpr const char* kSlintLiterals[] = {
X(remote_unlock_requires_secure_desktop) \
X(settings) \
X(language) \
X(screen_capture_method) \
X(screen_capture_method_auto) \
X(video_settings) \
X(remote_update_title) \
X(remote_update) \
@@ -3,6 +3,7 @@
#include <SDL3/SDL_audio.h>
#include <SDL3/SDL_error.h>
#include <SDL3/SDL_init.h>
#include <SDL3/SDL_timer.h>
#include <display_stream_id.h>
#include <remote_action.h>
@@ -50,7 +51,7 @@ SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner)
owner_.WithController(
id, [&](MiniRtcPeerPtr* peer, const std::string& wire) {
frame.captured_timestamp = MiniRtcGetSystemTimeMicros(peer);
return MiniRtcSendAudioFrameToPeer(peer, &frame,
return PeerSendAudioFrameToPeer(peer, &frame,
owner_.audio_label_.c_str(),
wire.data(), wire.size());
});
@@ -58,6 +59,19 @@ SessionDeviceManager::SessionDeviceManager(GuiRuntime &owner)
SessionDeviceManager::~SessionDeviceManager() { speaker_capture_.Shutdown(); }
#ifdef _WIN32
bool SessionDeviceManager::OwnsNativeCursor() const {
const auto* windows = dynamic_cast<ScreenCapturerWin*>(screen_capturer_);
return windows && windows->OwnsNativeCursor();
}
void SessionDeviceManager::SetNativeCursorPolicy(bool default_enabled, bool current_enabled) {
owner_.host_cursor_default_ = default_enabled;
owner_.host_cursor_preference_ = current_enabled;
if (auto* windows = dynamic_cast<ScreenCapturerWin*>(screen_capturer_))
windows->SetNativeCursorPolicy(default_enabled, current_enabled);
}
#endif
bool SessionDeviceManager::ShouldSendCapturedFrame(
std::chrono::steady_clock::time_point now, int fps) {
const auto interval =
@@ -148,7 +162,8 @@ int SessionDeviceManager::InitializeScreenCapturer() {
screen_capturer_->SetPrivacyController(&owner_.privacy_);
#ifdef _WIN32
if (auto windows = dynamic_cast<ScreenCapturerWin*>(screen_capturer_)) {
windows->SetCaptureMethod(owner_.config_center_->GetScreenCaptureMethod());
windows->SetUnattendedHost(owner_.unattended_host_);
windows->SetNativeCursorPolicy(owner_.host_cursor_default_, owner_.host_cursor_preference_);
}
#endif
@@ -176,6 +191,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
const auto now_time = std::chrono::steady_clock::now();
bool from_secure_desktop = false;
#ifdef _WIN32
owner_.host_capture_tick_ = SDL_GetTicks();
from_secure_desktop =
ScreenCapturerWin::CurrentFrameIsFromSecureDesktop();
#endif
@@ -236,6 +252,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
const auto send_started = std::chrono::steady_clock::now();
uint64_t send_errors = 0;
for (const std::string &remote_id : connected_remote_ids) {
if (owner_.managed_host_frontend_ && remote_id.rfind("lan/", 0) != 0) continue;
if (!owner_.preview_subscriptions_.ShouldSend(remote_id, now_time,
stream_id)) continue;
if (owner_.WithController(
@@ -243,7 +260,7 @@ int SessionDeviceManager::InitializeScreenCapturer() {
if (resumed_after_gap || stream_changed)
MiniRtcRequestVideoKeyFrame(peer, stream_id.c_str());
frame.captured_timestamp = MiniRtcGetSystemTimeMicros(peer);
return MiniRtcSendVideoFrameToPeer(peer, &frame, stream_id.c_str(),
return PeerSendVideoFrameToPeer(peer, &frame, stream_id.c_str(),
wire.data(), wire.size());
}) != 0)
++send_errors;
@@ -702,14 +719,20 @@ void SessionDeviceManager::UpdateInteractions() {
#endif
if (owner_.start_screen_capturer_ && !owner_.screen_capturer_is_started_) {
if (StartScreenCapturer() == 0) {
owner_.screen_capturer_is_started_ = true;
const auto now = std::chrono::steady_clock::now();
if (now >= next_capture_start_retry_) {
next_capture_start_retry_ = now + std::chrono::seconds(1);
if (StartScreenCapturer() == 0) {
owner_.screen_capturer_is_started_ = true;
next_capture_start_retry_ = {};
}
}
} else if (!owner_.start_screen_capturer_ &&
owner_.screen_capturer_is_started_) {
StopScreenCapturer();
owner_.screen_capturer_is_started_ = false;
}
if (!owner_.start_screen_capturer_) next_capture_start_retry_ = {};
UpdateScreenSharingPower();
@@ -66,6 +66,8 @@ public:
return remote_mouse_buttons_.Apply(remote_id, flag, std::move(send));
}
#ifdef _WIN32
bool OwnsNativeCursor() const;
void SetNativeCursorPolicy(bool default_enabled, bool current_enabled);
std::string SendServiceMouseCommand(int x, int y, int wheel, int flag);
#endif
// Requests are asynchronous; Stop drains/revokes audio delivery before
@@ -145,6 +147,7 @@ private:
#endif
std::chrono::steady_clock::time_point last_frame_time_{};
std::chrono::steady_clock::time_point next_frame_deadline_{};
std::chrono::steady_clock::time_point next_capture_start_retry_{};
std::mutex capture_metrics_mutex_;
std::chrono::steady_clock::time_point capture_metrics_started_{};
std::chrono::steady_clock::time_point last_capture_callback_{};
@@ -126,7 +126,7 @@ void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
auto callback = events->EnterCallback();
if (!callback || !events->DataAllowed()) return -1;
const auto wire_id = events->WireId(file.remote_id);
return MiniRtcSendReliableDataFrameToPeer(
return PeerSendReliableDataFrameToPeer(
peer, data, size, file.file_label.c_str(),
wire_id.data(), wire_id.size());
}, file_transfer::Session::Progress{});
@@ -176,7 +176,7 @@ void FileTransferManager::ProcessQueue(std::shared_ptr<RemoteSession> props) {
// Release that reservation even though no data chunk was ever sent.
const auto packet = EncodeFileCancel(id, 0);
const auto wire_id = events->WireId(file.remote_id);
MiniRtcSendReliableDataFrameToPeer(peer, packet.data(), packet.size(),
PeerSendReliableDataFrameToPeer(peer, packet.data(), packet.size(),
file.file_label.c_str(), wire_id.data(), wire_id.size());
}
}
@@ -382,7 +382,7 @@ std::shared_ptr<FileReceiver> FileTransferManager::ReceiverFor(
if (!callback || !events->DataAllowed()) return -1;
const auto bytes = EncodeFileTransferAck(ack);
const auto wire_id = events->WireId(remote);
return MiniRtcSendReliableDataFrameToPeer(
return PeerSendReliableDataFrameToPeer(
peer, bytes.data(), bytes.size(), kFileFeedbackStream,
wire_id.data(), wire_id.size());
});
@@ -533,7 +533,7 @@ bool FileTransferManager::SendBrowser(const std::string& remote, const FileBrows
const auto data = message.dump(-1, ' ', false, FileBrowser::Json::error_handler_t::replace);
if (!peer || data.size() > 60000) return false;
const auto wire_id = events->WireId(remote);
return MiniRtcSendReliableDataFrameToPeer(peer, data.data(), data.size(),
return PeerSendReliableDataFrameToPeer(peer, data.data(), data.size(),
owner_.control_data_label_.c_str(), wire_id.data(), wire_id.size()) == 0;
}
@@ -34,9 +34,6 @@
namespace crossdesk {
namespace {
constexpr uint32_t kCacheV3Magic = 0x33444358; // "XCD3"
constexpr uint32_t kCacheV3Version = 3;
#if defined(__linux__) && !defined(__OHOS__)
constexpr const char *kCredentialCacheNames[] = {
"secure_cache_v3.enc", "secure_cache_v2.enc", "secure_cache.enc"};
@@ -132,16 +129,6 @@ void CopyString(char (&destination)[Size], const char *source) {
}
}
uint32_t CacheChecksum(const void *data, size_t size) {
const auto *bytes = static_cast<const unsigned char *>(data);
uint32_t hash = 2166136261u;
for (size_t i = 0; i < size; ++i) {
hash ^= bytes[i];
hash *= 16777619u;
}
return hash;
}
std::filesystem::path TemporaryPathFor(
const std::filesystem::path &target) {
const auto timestamp = std::chrono::steady_clock::now()
@@ -259,6 +246,15 @@ bool WriteFileAtomically(const std::filesystem::path &target,
SettingsManager::SettingsManager(GuiRuntime &owner) : owner_(owner) {}
bool SettingsManager::ApplyHostIdentity(const std::string& identity, bool self_hosted) {
if (identity.size() != 16 || identity[9] != '@' ||
identity.substr(0, 9).find_first_not_of("0123456789") != std::string::npos)
return false;
std::lock_guard lock(cache_mutex_);
ActivateIdentity(identity.c_str(), self_hosted);
return SaveLocked() == 0;
}
int SettingsManager::Save() {
std::lock_guard<std::mutex> lock(cache_mutex_);
return SaveLocked();
@@ -271,8 +267,8 @@ int SettingsManager::SaveLocked() {
std::memcpy(cache_v2_.iv, owner_.aes128_iv_, sizeof(owner_.aes128_iv_));
CopyString(cache_v2_.self_hosted_id, owner_.self_hosted_id_);
cache_v3_.magic = kCacheV3Magic;
cache_v3_.version = kCacheV3Version;
cache_v3_.magic = credential_cache::kMagic;
cache_v3_.version = credential_cache::kVersion;
cache_v3_.base = cache_v2_;
cache_v3_.pending_identity[sizeof(cache_v3_.pending_identity) - 1] = '\0';
cache_v3_.pending_server_host[sizeof(cache_v3_.pending_server_host) - 1] =
@@ -280,7 +276,7 @@ int SettingsManager::SaveLocked() {
cache_v3_.pending_request_id[sizeof(cache_v3_.pending_request_id) - 1] =
'\0';
cache_v3_.checksum =
CacheChecksum(&cache_v3_, offsetof(CacheV3, checksum));
credential_cache::Checksum(&cache_v3_, offsetof(CacheV3, checksum));
if (!WriteFileAtomically(owner_.cache_path_ + "/secure_cache_v3.enc",
&cache_v3_, sizeof(cache_v3_))) {
@@ -302,7 +298,7 @@ int SettingsManager::SaveLocked() {
LOG_WARN("Failed to update legacy v1 credential cache");
}
return 0;
return owner_.PublishSharedIdentity() ? 0 : -1;
}
bool SettingsManager::ReadV3Locked() {
@@ -325,9 +321,7 @@ bool SettingsManager::ReadV3Locked() {
return false;
}
#endif
if (loaded.magic != kCacheV3Magic ||
loaded.version != kCacheV3Version ||
loaded.checksum != CacheChecksum(&loaded, offsetof(CacheV3, checksum))) {
if (!credential_cache::Valid(loaded)) {
LOG_WARN("Ignore invalid v3 credential cache");
return false;
}
@@ -7,6 +7,7 @@
#include <unordered_map>
#include "thumbnail.h"
#include "common/credential_cache.h"
namespace crossdesk {
@@ -21,6 +22,7 @@ public:
int Save();
int Load();
std::string ActiveIdentity() const;
bool ApplyHostIdentity(const std::string& identity, bool self_hosted);
int LoadRecentConnectionAliases();
int SaveRecentConnectionAliases() const;
@@ -56,46 +58,9 @@ public:
bool ClearPendingPasswordChange();
private:
struct CacheV1 {
char client_id_with_password[17];
int language;
// Reserved legacy slots: retain the on-disk cache layout.
int legacy_video_quality;
int legacy_video_frame_rate;
int video_encode_format;
bool legacy_enable_hardware_video_codec;
bool enable_turn;
bool enable_srtp;
unsigned char key[16];
unsigned char iv[16];
};
struct CacheV2 {
char client_id_with_password[17];
int language;
// Reserved legacy slots: retain the on-disk cache layout.
int legacy_video_quality;
int legacy_video_frame_rate;
int video_encode_format;
bool legacy_enable_hardware_video_codec;
bool enable_turn;
bool enable_srtp;
unsigned char key[16];
unsigned char iv[16];
char self_hosted_id[17];
};
struct CacheV3 {
uint32_t magic;
uint32_t version;
CacheV2 base;
char pending_identity[17];
char pending_server_host[256];
int pending_server_port;
bool pending_self_hosted;
char pending_request_id[64];
uint32_t checksum;
};
using CacheV1 = credential_cache::CacheV1;
using CacheV2 = credential_cache::CacheV2;
using CacheV3 = credential_cache::CacheV3;
int SaveLocked();
bool ReadV3Locked();
@@ -79,7 +79,7 @@ void GuiRuntime::HandleConnectionStatusChange() {
const auto message =
BuildDevicePresenceRequest(client_id_, recent_connection_ids_, true, {},
gallery_presence_ids_).dump();
if (MiniRtcSendSignalMessage(peer_, message.data(), message.size()) == 0) {
if (PeerSendSignalMessage(peer_, message.data(), message.size()) == 0) {
next_presence_refresh_at_ = now + kPresenceRefreshInterval;
} else {
need_to_send_recent_connections_ = true;
@@ -200,7 +200,8 @@ void GuiRuntime::HandleServerControllerDisconnected(
std::unique_lock lock(connection_status_mutex_);
connection_status_.erase(remote_id);
disconnect_lock_.Disconnected(remote_id,
config_center_->IsLockOnDisconnect());
config_center_->IsLockOnDisconnect() &&
!(managed_host_frontend_ && remote_id.rfind("lan/", 0) != 0));
remote_input_.Disconnect(remote_id);
devices_.OnControllerConnectionsChanged();
connection_host_names_.erase(remote_id);
@@ -296,7 +297,7 @@ int GuiRuntime::RequestSingleDevicePresence(const std::string& remote_id,
gallery_presence_ids_)
.dump();
int ret = MiniRtcSendSignalMessage(peer_, s.data(), s.size());
int ret = PeerSendSignalMessage(peer_, s.data(), s.size());
if (ret != 0) {
std::lock_guard<std::mutex> lock(pending_presence_probe_mutex_);
pending_presence_probe_ = false;
@@ -315,7 +316,7 @@ int GuiRuntime::ForceSignalReconnect() {
LOG_WARN("Signal reconnect requested with no connection peer");
return -1;
}
return MiniRtcReconnectSignal(peer_);
return PeerReconnectSignal(peer_);
}
void GuiRuntime::CloseRemoteSession(std::shared_ptr<RemoteSession> props) {
@@ -430,7 +431,7 @@ void GuiRuntime::CloseServerController(const std::string& remote_id) {
return;
controller_cleanup_tasks_[remote_id] = session_cleanup_queue_.PostTask(
[peer = peer_, transmission_id = std::string(client_id_), remote_id] {
if (MiniRtcDisconnectPeerConnection(peer, transmission_id.c_str(),
if (PeerDisconnectPeerConnection(peer, transmission_id.c_str(),
remote_id.c_str()) != 0) {
LOG_WARN("[{}] Could not queue controller disconnect notification",
remote_id);
@@ -460,6 +461,14 @@ void GuiRuntime::CloseConnectionPeer() {
}
for (const auto& id : controllers)
HandleServerControllerDisconnected(id, "peer_closed");
#ifdef _WIN32
{
std::lock_guard lock(local_host_state_mutex_);
mirrored_controllers_.clear();
local_controller_info_.clear();
local_host_status_.controllers_dirty = true;
}
#endif
signal_connected_ = false;
device_presence_cache_.SetSignalConnected(false);
signal_status_ = MiniRtcSignalStatus::MiniRtcSignalClosed;
@@ -474,7 +483,8 @@ void GuiRuntime::CloseConnectionPeer() {
[peer, handler = peer_events_, id = std::string(client_id_)]() mutable {
handler->Deactivate();
LOG_INFO("[{}] Background connection peer cleanup", id);
MiniRtcLeaveConnection(peer, id.c_str());
if (!IsLocalPeer(peer)) MiniRtcLeaveConnection(peer, id.c_str());
UnbindLocalPeer(peer);
MiniRtcDestroyPeer(&peer);
});
}
@@ -14,6 +14,12 @@ void GuiRuntime::InitializeTrustedDevices(const std::filesystem::path& directory
}
bool GuiRuntime::TrustedControllerAllowed(const std::string& id) {
#ifdef _WIN32
if (managed_host_frontend_ && id.rfind("lan/", 0) != 0) {
std::lock_guard lock(local_host_state_mutex_);
return mirrored_controllers_.contains(id);
}
#endif
if (!config_center_ || !config_center_->IsTrustedDevicesOnly()) return true;
std::lock_guard lock(controller_trust_mutex_);
const auto it = controller_trust_.find(id);
@@ -21,6 +27,7 @@ bool GuiRuntime::TrustedControllerAllowed(const std::string& id) {
}
bool GuiRuntime::BeginControllerTrust(const std::string& id, PeerEventHandler* events) {
if (managed_host_frontend_ && !events->IsDirect()) return TrustedControllerAllowed(id);
const bool required = config_center_->IsTrustedDevicesOnly();
std::lock_guard lock(controller_trust_mutex_);
const auto previous = controller_trust_.find(id);
@@ -54,7 +61,7 @@ void GuiRuntime::ForgetControllerTrust(const std::string& id, PeerEventHandler*
int GuiRuntime::SendTrustMessage(const std::string& id, const nlohmann::json& message) {
const auto text = message.dump();
return WithController(id, [&](MiniRtcPeerPtr* peer, const std::string& wire) {
return MiniRtcSendReliableDataFrameToPeer(peer, text.data(), text.size(),
return PeerSendReliableDataFrameToPeer(peer, text.data(), text.size(),
control_data_label_.c_str(), wire.data(), wire.size());
}, false);
}
@@ -94,7 +101,7 @@ bool GuiRuntime::HandleDeviceTrustMessage(const std::string& id, PeerEventHandle
const auto wire = events->WireId(id);
const auto proof = trusted_devices_->Prove(message.at("nonce"),
props->local_id_, GetHostName()).dump();
MiniRtcSendReliableDataFrameToPeer(props->peer_, proof.data(), proof.size(),
PeerSendReliableDataFrameToPeer(props->peer_, proof.data(), proof.size(),
control_data_label_.c_str(), wire.data(), wire.size());
} else if (type == "device_trust_proof") {
std::lock_guard lock(controller_trust_mutex_);
@@ -127,6 +134,7 @@ void GuiRuntime::TickControllerTrust() {
{
std::lock_guard lock(controller_trust_mutex_);
for (auto& [id, state] : controller_trust_) {
if (managed_host_frontend_ && state.events == peer_events_.get()) continue;
if (state.closing) continue;
const bool trusted = state.IsTrusted(trusted_devices_.get());
if (required && (state.invalid || (state.verified && !trusted) ||
@@ -179,14 +187,28 @@ bool GuiRuntime::TrustController(const std::string& id, const std::string& expec
if (!trusted_devices_ || !IsAuthorizedController(id)) return false;
const auto device = VerifiedController(id);
if (!device || device->public_key != expected_key) return false;
try { trusted_devices_->Add(*device); return true; }
return AddTrustedDevice(*device);
}
bool GuiRuntime::AddTrustedDevice(const TrustedDevices::Device& device) {
#ifdef _WIN32
if (managed_host_frontend_) return SendLocalHostCommand({{"op", "trust_add"},
{"key", device.public_key}, {"name", device.name}, {"device", device.device_id}});
#endif
if (!trusted_devices_) return false;
try { trusted_devices_->Add(device); }
catch (const std::exception& error) { LOG_ERROR("Cannot trust device: {}", error.what()); return false; }
// Local persistence completes before publishing. A shared-store failure
// must not leave the in-memory trust list behind its own on-disk state.
return PublishSharedIdentity();
}
bool GuiRuntime::RemoveTrustedDevice(const std::string& public_key) {
#ifdef _WIN32
if (managed_host_frontend_) return SendLocalHostCommand({{"op", "trust_remove"}, {"key", public_key}});
#endif
if (!trusted_devices_) return false;
try { trusted_devices_->Remove(public_key); }
catch (const std::exception& error) { LOG_ERROR("Cannot remove trusted device: {}", error.what()); return false; }
TickControllerTrust();
return true;
return PublishSharedIdentity();
}
} // namespace crossdesk
+86 -2
View File
@@ -20,6 +20,8 @@
#include "rd_log.h"
#ifdef _WIN32
#include "platform/windows/input/windows_input_injector.h"
#include "platform/windows/service/autostart_control.h"
#include "service_host.h"
#endif
#if defined(__linux__) && !defined(__OHOS__)
#include "platform/linux/headless/headless_console.h"
@@ -131,6 +133,63 @@ void GuiRuntime::SendRemoteInput(const RemoteAction& action) {
}
}
bool GuiRuntime::PrepareSharedGuiIdentity(bool import_existing) {
#ifdef _WIN32
if (!SupportsServiceAutostart() || shared_identity_) return true;
if (!import_existing && settings_.Save() != 0) return false;
if (peer_) {
CloseConnectionPeer();
WaitForSessionCleanup();
}
std::string error;
if (!import_existing && !SharedHostIdentity::Prepare(
path_manager_->GetCachePath(), path_manager_->GetConfigPath() / "trusted-devices",
config_center_->ExportSharedHostConfig(), error)) {
LOG_ERROR("Cannot prepare persistent host: {}", error);
return false;
}
auto identity = SharedHostIdentity::AttachGui(path_manager_->GetCachePath(),
path_manager_->GetConfigPath() / "trusted-devices", import_existing, error);
if (!identity ||
(import_existing && config_center_->ApplySharedHostConfig(identity->HostConfig()) != 0)) {
LOG_ERROR("Cannot load the machine identity: {}", error);
return false;
}
shared_identity_ = std::move(identity);
managed_host_frontend_ = true;
StartCrossDeskService();
#else
(void)import_existing;
#endif
return true;
}
bool GuiRuntime::PublishSharedIdentity(bool include_config) {
#ifdef _WIN32
if (managed_host_frontend_) {
if (!include_config) return true;
pending_local_config_ =
{{"op", "config"}, {"config", config_center_->ExportSharedHostConfig()},
{"privacy_on_connect", config_center_->IsEnablePrivacyScreen()},
{"request", ++next_local_config_request_}};
local_config_sent_ = false;
return true; // Tick sends after startup/reattachment; errors return to the UI.
}
if (shared_identity_) {
local_host_profile_dirty_ = true;
std::string error;
if (!shared_identity_->Publish(include_config ? config_center_->ExportSharedHostConfig()
: std::string{}, error)) {
LOG_ERROR("Cannot synchronize the shared device identity: {}", error);
return false;
}
}
#else
(void)include_config;
#endif
return true;
}
int GuiRuntime::CreateConnectionPeer() {
// Tick retries after the old peer's leave notification and socket shutdown.
if (connection_peer_cleanup_.valid()) return 0;
@@ -286,12 +345,38 @@ int GuiRuntime::CreateConnectionPeer() {
need_to_send_recent_connections_ = true;
signal_status_ = MiniRtcSignalStatus::MiniRtcSignalConnecting;
// Complete slow headless display setup before connecting to signaling.
if (unattended_host_ && devices_.EnsureCaptureReady() != 0) return -1;
{
std::unique_lock lock(remote_sessions_mutex_);
peer_ = MiniRtcCreatePeer(&params_);
}
if (peer_) {
LOG_INFO("Create peer instance [{}] successful", client_id_);
#ifdef _WIN32
if (managed_host_frontend_) {
if (!local_host_) local_host_ = std::make_shared<LocalHostChannel>(false);
std::weak_ptr<LocalHostChannel> channel = local_host_;
BindLocalPeer(peer_, [this, channel](nlohmann::json message) {
if (message.contains("id")) {
std::lock_guard lock(local_host_state_mutex_);
const auto found = mirrored_controllers_.find(message.at("id").get<std::string>());
if (found == mirrored_controllers_.end()) return false;
message["epoch"] = found->second;
}
const auto connection = channel.lock();
return connection && connection->Send(message);
});
local_host_->Send({{"op", "hello"},
{"privacy_on_connect", config_center_->IsEnablePrivacyScreen()},
{"privacy_requested", privacy_.Requested() || config_center_->IsEnablePrivacyScreen()}});
// Outgoing viewer/direct sessions still register their native streams
// from the user's local display metadata; this does not start capture.
devices_.EnsureCaptureReady();
LOG_INFO("GUI attached to persistent host; no second signaling login");
return 0;
}
#endif
MiniRtcInit(peer_);
LOG_INFO("Peer [{}] init finish", client_id_);
} else {
@@ -313,8 +398,7 @@ int GuiRuntime::CreateConnectionPeer() {
MiniRtcAddDataStream(peer_, file_label_.c_str(), true);
MiniRtcAddDataStream(peer_, file_feedback_label_.c_str(), true);
MiniRtcAddDataStream(peer_, clipboard_label_.c_str(), true);
if (!unattended_host_)
MiniRtcAddDataStream(peer_, web_clipboard::kStream, true);
MiniRtcAddDataStream(peer_, web_clipboard::kStream, true);
return 0;
} else {
return -1;
@@ -27,11 +27,16 @@
#include "runtime/disconnect_lock_policy.h"
#include "runtime/announcement_inbox.h"
#include "runtime/peer_event_handler.h"
#include "runtime/peer_transport.h"
#include "runtime/privacy_session_policy.h"
#include "runtime/trusted_devices.h"
#ifdef __APPLE__
#include "platform/macos/gui/runtime/mac_permission_migration.h"
#endif
#ifdef _WIN32
#include "local_host_channel.h"
#include "runtime/local_host_status.h"
#endif
namespace crossdesk {
@@ -114,6 +119,8 @@ class GuiRuntime : protected gui_detail::GuiState {
void HandleScreenLock();
bool IsAuthorizedController(const std::string& remote_id);
void InitializeTrustedDevices(const std::filesystem::path& directory);
bool PublishSharedIdentity(bool include_config = false);
bool PrepareSharedGuiIdentity(bool import_existing);
bool TrustedControllerAllowed(const std::string& remote_id);
bool BeginControllerTrust(const std::string& remote_id, PeerEventHandler* events);
void ForgetControllerTrust(const std::string& remote_id, PeerEventHandler* events);
@@ -122,6 +129,7 @@ class GuiRuntime : protected gui_detail::GuiState {
void TickControllerTrust();
std::optional<TrustedDevices::Device> VerifiedController(const std::string& remote_id);
bool TrustController(const std::string& remote_id, const std::string& expected_key);
bool AddTrustedDevice(const TrustedDevices::Device& device);
bool RemoveTrustedDevice(const std::string& public_key);
int SendTrustMessage(const std::string& remote_id, const nlohmann::json& message);
std::unique_ptr<TrustedDevices> trusted_devices_;
@@ -201,6 +209,39 @@ class GuiRuntime : protected gui_detail::GuiState {
PrivacyController privacy_;
// Set before initializing callbacks; immutable for the process lifetime.
bool unattended_host_ = false;
bool managed_host_frontend_ = false;
std::string BuildHostInformation(bool advertise_trust = false);
void AnnounceHostCapabilities(const std::string& id);
#ifdef _WIN32
std::shared_ptr<LocalHostChannel> local_host_;
std::atomic<bool> local_gui_ready_{false};
std::mutex local_host_state_mutex_;
std::map<std::string, nlohmann::json> local_controller_info_;
std::map<std::string, uint64_t> mirrored_controllers_;
uint64_t next_controller_epoch_ = 0;
uint64_t next_local_host_status_ = 0;
LocalHostStatus local_host_status_;
std::atomic<bool> local_host_profile_dirty_{true};
nlohmann::json pending_local_config_;
uint64_t next_local_config_request_ = 0;
bool local_config_sent_ = false;
std::optional<bool> local_cursor_hidden_sent_;
std::optional<bool> local_cursor_request_sent_;
bool local_cursor_policy_ready_ = false;
bool host_cursor_default_ = true, host_cursor_preference_ = true;
std::atomic<uint64_t> host_capture_tick_{0};
bool ForwardUserSessionData(const std::string& id, const std::string& stream,
const char* data, size_t size);
void PublishLocalHostStatus();
bool SendLocalHostCommand(nlohmann::json message);
#endif
bool UserSessionFeaturesAvailable() const {
#ifdef _WIN32
return !unattended_host_ || local_gui_ready_.load();
#else
return !unattended_host_;
#endif
}
AnnouncementInbox announcements_;
PrivacySessionPolicy privacy_sessions_;
DisconnectLockPolicy disconnect_lock_;
+1 -1
View File
@@ -199,7 +199,7 @@ int GuiRuntime::WithController(
int GuiRuntime::SendControllerData(const std::string& id, const char* data,
size_t size, const char* label) {
return WithController(id, [&](MiniRtcPeerPtr* peer, const std::string& wire) {
return MiniRtcSendReliableDataFrameToPeer(peer, data, size, label, wire.data(),
return PeerSendReliableDataFrameToPeer(peer, data, size, label, wire.data(),
wire.size());
});
}
@@ -0,0 +1,43 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-11-09
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LOCAL_HOST_STATUS_H_
#define _LOCAL_HOST_STATUS_H_
#include <nlohmann/json.hpp>
namespace crossdesk {
// Pending sections survive deferred settings and failed local persistence.
// Clear each flag only after its section has been applied successfully.
struct LocalHostStatus {
nlohmann::json values = nlohmann::json::object();
bool config_dirty = true;
bool identity_dirty = true;
bool controllers_dirty = true;
bool trusted_dirty = true;
void Reset() {
values.clear();
config_dirty = identity_dirty = controllers_dirty = trusted_dirty = true;
}
void Merge(const nlohmann::json& update) {
const auto changed = [&](const char* field) {
return update.contains(field) &&
(!values.contains(field) || update.at(field) != values.at(field));
};
config_dirty |= changed("config");
identity_dirty |= changed("identity") || changed("self_hosted");
controllers_dirty |= changed("controllers");
trusted_dirty |= changed("trusted");
values.update(update);
}
};
} // namespace crossdesk
#endif
@@ -139,6 +139,10 @@ void PeerEventHandler::OnReceiveDataBuffer(
const auto viewer = runtime->FindRemoteSession(remote_id);
if (viewer && viewer->peer_events_.get() != handler) return;
if (!viewer && !runtime->TrustedControllerAllowed(remote_id)) return;
#ifdef _WIN32
if (runtime->unattended_host_ && !viewer &&
runtime->ForwardUserSessionData(remote_id, source_id, data, size)) return;
#endif
// The service-owned host runs as SYSTEM. Do not expose filesystem or
// clipboard operations in that security context.
if (runtime->unattended_host_ &&
@@ -198,6 +202,11 @@ void PeerEventHandler::OnReceiveDataBuffer(
return;
const auto subscription = PreviewSubscription::Parse(preview_json);
if (!subscription) return;
if (runtime->managed_host_frontend_ && !handler->IsDirect()) {
runtime->preview_subscriptions_.Set(remote_id, *subscription);
if (subscription->audio) runtime->devices_.StartSpeakerCapturer();
return; // The persistent host has already applied/acknowledged this.
}
const auto previous = runtime->preview_subscriptions_.Get(remote_id);
bool accepted = true;
// Apply encoder quality to this controller only; other controllers retain
@@ -240,7 +249,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
{"request_id", subscription->request_id},
{"accepted",
accepted}}.dump();
MiniRtcSendReliableDataFrameToPeer(bound_peer, reply.data(), reply.size(),
PeerSendReliableDataFrameToPeer(bound_peer, reply.data(), reply.size(),
runtime->control_data_label_.c_str(),
wire_id.data(), wire_id.size());
return;
@@ -324,7 +333,7 @@ void PeerEventHandler::OnReceiveDataBuffer(
settings.frame_rate, settings.preference, remote_action.vs.accepted);
remote_action.type = ControlType::video_settings_status;
const auto message = remote_action.to_json();
MiniRtcSendReliableDataFrameToPeer(bound_peer, message.data(), message.size(),
PeerSendReliableDataFrameToPeer(bound_peer, message.data(), message.size(),
runtime->control_data_label_.c_str(),
wire_id.data(), wire_id.size());
return;
@@ -526,6 +535,10 @@ void PeerEventHandler::OnReceiveDataBuffer(
}
} else {
if (remote_action.type == ControlType::audio_capture) {
if (runtime->managed_host_frontend_ && !handler->IsDirect()) {
if (remote_action.a) runtime->devices_.StartSpeakerCapturer();
return;
}
// A negotiated subscription remains authoritative if a legacy capture
// command arrives after it (e.g. when unmuting an inactive window).
const auto subscription = runtime->preview_subscriptions_.Get(remote_id);
@@ -586,8 +599,10 @@ InputOutcome GuiRuntime::ApplyRemoteInput(const std::string& remote_id,
const int display = selected_display_.load();
if (is_server_mode_ && remote_action.type == ControlType::mouse) {
std::lock_guard lock(remote_pointer_input_mutex_);
last_remote_pointer_input_time_[remote_id] =
std::chrono::steady_clock::now();
const auto now = std::chrono::steady_clock::now();
auto [position, first] = last_remote_pointer_input_time_.try_emplace(remote_id, now);
position->second = now;
if (first) LOG_INFO("Remote pointer input received: peer={}", remote_id);
}
if (remote_action.type == ControlType::mouse) {
@@ -652,6 +667,12 @@ InputOutcome GuiRuntime::InjectRemoteMouse(const RemoteAction& remote_action,
static_cast<int>(remote_action.m.flag));
auto json = nlohmann::json::parse(response, nullptr, false);
const bool sent = json.is_object() && json.value("ok", false);
static thread_local bool logged_service_input = false;
if (sent && !logged_service_input) {
logged_service_input = true;
LOG_INFO("Secure desktop pointer injection succeeded: stage='{}', desktop='{}'",
WindowsInputStage(), json.value("desktop", ""));
}
const std::string error =
json.is_object() ? json.value("error", "") : "";
return {sent, IsDesktopInputSetupPending(error)};
@@ -75,7 +75,7 @@ void PeerEventHandler::SendClientInfo(MiniRtcPeerPtr* peer,
{"version", CROSSDESK_VERSION},
{"platform", HostPlatformName(kClientPlatform)}};
const std::string payload = message.dump();
if (MiniRtcSendSignalMessage(peer, payload.data(), payload.size()) != 0) {
if (PeerSendSignalMessage(peer, payload.data(), payload.size()) != 0) {
LOG_WARN("[{}] failed to report client information", client_id);
}
}
@@ -118,6 +118,12 @@ void PeerEventHandler::OnSignalMessage(const char* message, size_t size,
return;
}
std::string type = j["type"].get<std::string>();
#ifdef _WIN32
if (runtime->unattended_host_ && runtime->local_host_ &&
(type == "presence" || type == "presence_update" ||
type == "announcements" || type == "announcements_changed"))
runtime->local_host_->Send({{"op", "signal"}, {"message", s}});
#endif
if (type == "user_join_transmission" && j.contains("status") &&
j["status"] == "failed" && j.contains("transmission_id") &&
j["transmission_id"].is_string()) {
@@ -329,6 +335,19 @@ void PeerEventHandler::DispatchConnectionStatus(MiniRtcConnectionStatus status,
} else {
runtime->ForgetControllerTrust(remote_id, handler);
}
#ifdef _WIN32
if (runtime->unattended_host_) {
std::lock_guard lock(runtime->local_host_state_mutex_);
if (status == MiniRtcConnectionStatus::MiniRtcConnectionConnected) {
auto& info = runtime->local_controller_info_[remote_id];
if (!info.contains("epoch")) info["epoch"] = ++runtime->next_controller_epoch_;
} else if (status == MiniRtcConnectionStatus::MiniRtcConnectionClosed ||
status == MiniRtcConnectionStatus::MiniRtcConnectionFailed ||
status == MiniRtcConnectionStatus::MiniRtcConnectionDisconnected) {
runtime->local_controller_info_.erase(remote_id);
}
}
#endif
}
if (props) {
@@ -348,25 +367,7 @@ void PeerEventHandler::DispatchConnectionStatus(MiniRtcConnectionStatus status,
props->cursor_presentation_ = {};
}
{
RemoteAction remote_action = MakeHostInformation(
GetHostName(), runtime->devices_.host_display_list(),
/*supports_privacy_screen=*/!runtime->unattended_host_,
CROSSDESK_VERSION, kClientPlatform);
remote_action.i.supports_video_settings = true;
remote_action.i.supports_video_codec_status = true;
remote_action.i.supports_text_input = runtime->devices_.SupportsTextInput();
remote_action.i.supports_deskhub_preview = true;
remote_action.i.supports_deskhub_resolution = true;
remote_action.i.supports_deskhub_fps15 = true;
remote_action.i.supports_deskhub_bandwidth = true;
remote_action.i.supports_file_browser = !runtime->unattended_host_;
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
remote_action.i.display_list[i]);
}
auto information = nlohmann::json::parse(remote_action.to_json());
information["device_trust_version"] = 1;
std::string msg = information.dump();
const auto msg = runtime->BuildHostInformation(true);
const int ret =
MiniRtcSendReliableDataFrame(props->peer_, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
@@ -374,7 +375,6 @@ void PeerEventHandler::DispatchConnectionStatus(MiniRtcConnectionStatus status,
LOG_WARN("[{}] host information send failed, ret={}", remote_id,
ret);
}
FreeRemoteAction(remote_action);
}
if (!props->wall_managed_ && !runtime->need_to_create_stream_window_) {
@@ -517,43 +517,20 @@ void PeerEventHandler::DispatchConnectionStatus(MiniRtcConnectionStatus status,
#if _WIN32
runtime->last_windows_service_status_tick_ = 0;
#endif
{
RemoteAction remote_action = MakeHostInformation(
GetHostName(), runtime->devices_.host_display_list(),
/*supports_privacy_screen=*/!runtime->unattended_host_,
CROSSDESK_VERSION, kClientPlatform);
remote_action.i.supports_video_settings = true;
remote_action.i.supports_video_codec_status = true;
remote_action.i.supports_text_input = runtime->devices_.SupportsTextInput();
remote_action.i.supports_deskhub_preview = true;
remote_action.i.supports_deskhub_resolution = true;
remote_action.i.supports_deskhub_fps15 = true;
remote_action.i.supports_deskhub_bandwidth = true;
remote_action.i.supports_file_browser = !runtime->unattended_host_;
for (std::size_t i = 0; i < remote_action.i.display_num; i++) {
LOG_INFO("Local display [{}:{}]", i + 1,
remote_action.i.display_list[i]);
}
std::string msg = remote_action.to_json();
const int ret =
runtime->SendControllerData(remote_id, msg.data(), msg.size(),
runtime->control_data_label_.c_str());
if (ret != 0) {
LOG_WARN("[{}] host information send failed, ret={}", remote_id,
ret);
}
FreeRemoteAction(remote_action);
}
if (!runtime->managed_host_frontend_ || handler->IsDirect())
runtime->AnnounceHostCapabilities(remote_id);
runtime->need_to_destroy_server_window_.store(
false, std::memory_order_release);
runtime->need_to_create_server_window_.store(true,
std::memory_order_release);
runtime->is_server_mode_ = true;
runtime->start_screen_capturer_ = true;
if (!runtime->managed_host_frontend_ || handler->IsDirect())
runtime->start_screen_capturer_ = true;
runtime->devices_.StartSpeakerCapturer();
runtime->remote_client_id_ = remote_id;
runtime->start_mouse_controller_ = true;
if (!runtime->managed_host_frontend_ || handler->IsDirect())
runtime->start_mouse_controller_ = true;
break;
}
@@ -670,4 +647,33 @@ void PeerEventHandler::OnNetStatusReport(
props->net_traffic_stats_.Store(*net_traffic_stats);
}
}
std::string GuiRuntime::BuildHostInformation(bool advertise_trust) {
auto action = MakeHostInformation(GetHostName(), devices_.host_display_list(),
UserSessionFeaturesAvailable(), CROSSDESK_VERSION, kClientPlatform);
action.i.supports_video_settings = true;
action.i.supports_video_codec_status = true;
action.i.supports_text_input = devices_.SupportsTextInput();
action.i.supports_deskhub_preview = true;
action.i.supports_deskhub_resolution = true;
action.i.supports_deskhub_fps15 = true;
action.i.supports_deskhub_bandwidth = true;
action.i.supports_file_browser = UserSessionFeaturesAvailable();
for (std::size_t i = 0; i < action.i.display_num; ++i)
LOG_INFO("Local display [{}:{}]", i + 1, action.i.display_list[i]);
auto message = action.to_json();
FreeRemoteAction(action);
if (advertise_trust) {
auto information = nlohmann::json::parse(message);
information["device_trust_version"] = 1;
message = information.dump();
}
return message;
}
void GuiRuntime::AnnounceHostCapabilities(const std::string& id) {
const auto message = BuildHostInformation();
const int ret = SendControllerData(id, message.data(), message.size(), control_data_label_.c_str());
if (ret != 0) LOG_WARN("[{}] host information send failed, ret={}", id, ret);
}
} // namespace crossdesk
@@ -0,0 +1,104 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-11-09
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PEER_TRANSPORT_H_
#define _PEER_TRANSPORT_H_
#include "runtime/peer_transport.h"
#ifdef _WIN32
#include <map>
#include <mutex>
#endif
namespace crossdesk {
namespace {
using Json = nlohmann::json;
using LocalSender = std::function<bool(Json)>;
Json Bytes(const char* data, size_t size) {
return Json::binary(std::vector<uint8_t>(data, data + size));
}
#ifdef _WIN32
std::mutex routes_mutex;
std::map<MiniRtcPeerPtr*, LocalSender> routes;
LocalSender FindLocalSender(MiniRtcPeerPtr* peer) {
std::lock_guard lock(routes_mutex);
const auto found = routes.find(peer);
return found == routes.end() ? LocalSender{} : found->second;
}
#else
LocalSender FindLocalSender(MiniRtcPeerPtr*) { return {}; }
#endif
}
#ifdef _WIN32
void BindLocalPeer(MiniRtcPeerPtr* peer, std::function<bool(Json)> send) {
std::lock_guard lock(routes_mutex);
routes[peer] = std::move(send);
}
void UnbindLocalPeer(MiniRtcPeerPtr* peer) {
std::lock_guard lock(routes_mutex);
routes.erase(peer);
}
bool IsLocalPeer(MiniRtcPeerPtr* peer) {
std::lock_guard lock(routes_mutex);
return routes.contains(peer);
}
#else
void BindLocalPeer(MiniRtcPeerPtr*, std::function<bool(Json)>) {}
void UnbindLocalPeer(MiniRtcPeerPtr*) {}
bool IsLocalPeer(MiniRtcPeerPtr*) { return false; }
#endif
int PeerSendReliableDataFrameToPeer(MiniRtcPeerPtr* peer, const char* data,
size_t size, const char* stream, const char* id, size_t id_size) {
if (auto send = FindLocalSender(peer)) {
if (!data || !size || !stream || !id || !id_size) return -1;
return send({{"op", "data"}, {"id", std::string(id, id_size)},
{"stream", stream}, {"bytes", Bytes(data, size)}}) ? 0 : -1;
}
return MiniRtcSendReliableDataFrameToPeer(peer, data, size, stream, id, id_size);
}
int PeerSendAudioFrameToPeer(MiniRtcPeerPtr* peer, const MiniRtcAudioFrame* frame,
const char* stream, const char* id, size_t size) {
if (auto send = FindLocalSender(peer)) {
if (!frame || !frame->data || !frame->size || !stream || !id || !size) return -1;
return send({{"op", "audio"}, {"id", std::string(id, size)},
{"stream", stream}, {"bytes", Bytes(frame->data, frame->size)}}) ? 0 : -1;
}
return MiniRtcSendAudioFrameToPeer(peer, frame, stream, id, size);
}
int PeerSendVideoFrameToPeer(MiniRtcPeerPtr* peer, const MiniRtcVideoFrame* frame,
const char* stream, const char* id, size_t size) {
// The persistent host owns video. A GUI capturing for a direct LAN viewer
// must not send that second capture into its mirrored cloud connections.
return IsLocalPeer(peer) ? 0 : MiniRtcSendVideoFrameToPeer(peer, frame, stream, id, size);
}
int PeerSendSignalMessage(MiniRtcPeerPtr* peer, const char* data, size_t size) {
if (auto send = FindLocalSender(peer)) {
if (!data || !size) return -1;
return send({{"op", "signal"}, {"message", std::string(data, size)}}) ? 0 : -1;
}
return MiniRtcSendSignalMessage(peer, data, size);
}
int PeerReconnectSignal(MiniRtcPeerPtr* peer) {
if (auto send = FindLocalSender(peer)) return send({{"op", "reconnect"}}) ? 0 : -1;
return MiniRtcReconnectSignal(peer);
}
int PeerDisconnectPeerConnection(MiniRtcPeerPtr* peer, const char* host, const char* id) {
if (auto send = FindLocalSender(peer))
return send({{"op", "disconnect"}, {"id", id}}) ? 0 : -1;
return MiniRtcDisconnectPeerConnection(peer, host, id);
}
int PeerUpdateConnectionSettings(MiniRtcPeerPtr* peer, bool hardware, bool av1,
MiniRtcTurnMode turn) {
if (auto send = FindLocalSender(peer))
return send({{"op", "connection_settings"},
{"av1", av1}, {"turn", static_cast<int>(turn)}}) ? 0 : -1;
return MiniRtcUpdateConnectionSettings(peer, hardware, av1, turn);
}
} // namespace crossdesk
#endif
@@ -0,0 +1,34 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-11-09
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _PEER_TRANSPORT_H_
#define _PEER_TRANSPORT_H_
#include <minirtc.h>
#include <nlohmann/json.hpp>
#include <functional>
namespace crossdesk {
// A managed GUI has a local endpoint instead of another signaling login.
// Viewer peers and direct LAN peers continue to use MiniRTC normally.
void BindLocalPeer(MiniRtcPeerPtr*, std::function<bool(nlohmann::json)> send);
void UnbindLocalPeer(MiniRtcPeerPtr*);
bool IsLocalPeer(MiniRtcPeerPtr*);
int PeerSendReliableDataFrameToPeer(MiniRtcPeerPtr*, const char*, size_t,
const char*, const char*, size_t);
int PeerSendAudioFrameToPeer(MiniRtcPeerPtr*, const MiniRtcAudioFrame*,
const char*, const char*, size_t);
int PeerSendVideoFrameToPeer(MiniRtcPeerPtr*, const MiniRtcVideoFrame*,
const char*, const char*, size_t);
int PeerSendSignalMessage(MiniRtcPeerPtr*, const char*, size_t);
int PeerReconnectSignal(MiniRtcPeerPtr*);
int PeerDisconnectPeerConnection(MiniRtcPeerPtr*, const char*, const char*);
int PeerUpdateConnectionSettings(MiniRtcPeerPtr*, bool, bool, MiniRtcTurnMode);
} // namespace crossdesk
#endif
@@ -33,6 +33,7 @@
#if _WIN32
#include "platform/windows/service/service_query_worker.h"
#include "platform/windows/service/shared_identity.h"
#endif
namespace crossdesk::gui_detail {
@@ -47,6 +48,9 @@ struct InfrastructureState {
std::string exec_log_path_;
std::string dll_log_path_;
std::string cache_path_;
#ifdef _WIN32
std::unique_ptr<SharedHostIdentity> shared_identity_;
#endif
int localization_language_index_ = -1;
bool modules_inited_ = false;
unsigned char aes128_key_[16]{};
@@ -230,4 +230,16 @@ void TrustedDevices::Remove(const std::string& public_key) {
Save(next);
devices_.swap(next);
}
void TrustedDevices::Replace(const std::vector<Device>& devices) {
Require(devices.size() <= kMaxTrustedDevices);
std::map<std::string, Device> next;
for (const auto& device : devices) {
Require(IsHex(device.public_key, 32) && Label(device.name) && Label(device.device_id));
Require(next.emplace(device.public_key, device).second);
}
std::lock_guard lock(mutex_);
if (next == devices_) return;
Save(next);
devices_.swap(next);
}
} // namespace crossdesk
@@ -48,6 +48,9 @@ class TrustedDevices {
// Call only after a local add action for a verified peer or valid pairing info.
void Add(const Device& device);
void Remove(const std::string& public_key);
// Replace a complete snapshot from the authenticated machine host.
// Validation and persistence finish before the in-memory list changes.
void Replace(const std::vector<Device>& devices);
private:
void Save(const std::map<std::string, Device>& devices) const;
+10 -10
View File
@@ -73,8 +73,6 @@ export global UiStrings {
in-out property <string> delete-connection: "Delete connection";
in-out property <string> confirm-delete: "Delete this recent connection?";
in-out property <string> language: "Language";
in-out property <string> capture-method: "Capture method";
in-out property <string> capture-method-auto: "Auto";
in-out property <string> codec: "Video codec";
in-out property <string> pause-video-when-minimized: "Pause video when minimized";
in-out property <string> pause-video-when-minimized-hint: "Pause video reception when the control window is minimized to reduce local decoding resource usage.";
@@ -737,8 +735,6 @@ export component MainWindow inherits Window {
in-out property <int> language-index: 10;
in property <[string]> language-options;
in property <bool> capture-method-visible: false;
in-out property <int> capture-method-index: 0;
in-out property <int> codec-index: 0;
in-out property <bool> ip-direct-enabled-draft: true;
in-out property <bool> lan-listen-all-draft: true;
@@ -746,6 +742,9 @@ export component MainWindow inherits Window {
in-out property <bool> force-relay-enabled: false;
in-out property <bool> self-hosted-enabled: false;
in-out property <bool> autostart-enabled: false;
in property <bool> autostart-available: true;
in property <bool> service-autostart-platform: false;
in property <string> service-autostart-hint;
in property <bool> autostart-system-managed: false;
in property <AutostartStatus> autostart-status: AutostartStatus.checking;
out property <string> autostart-status-text:
@@ -2876,11 +2875,7 @@ export component MainWindow inherits Window {
}
SettingsGroup {
title: GalleryStrings.texts[115];
rows: root.capture-method-visible ? 3 : 2;
if root.capture-method-visible: SettingsRow {
label: UiStrings.capture-method; enabled: !root.settings-session-active;
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; }
}
rows: 2;
SettingsRow {
label: UiStrings.codec; enabled: !root.settings-session-active;
SettingsSelect { x: parent.width - 116px; y: 4px; width: 104px; enabled: !root.settings-session-active; model: [UiStrings.codec-h264, UiStrings.codec-av1]; current-index <=> root.codec-index; }
@@ -2983,7 +2978,7 @@ export component MainWindow inherits Window {
}
SettingsGroup {
title: GalleryStrings.texts[123]; rows: 2;
if !root.autostart-system-managed: SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: !root.settings-session-active; }
if !root.autostart-system-managed: SettingsToggleRow { label: UiStrings.autostart; checked <=> root.autostart-enabled; enabled: root.autostart-available && !root.settings-session-active; }
if root.autostart-system-managed: SettingsToggleRow {
label: UiStrings.autostart;
checked: root.autostart-status == AutostartStatus.enabled;
@@ -2992,6 +2987,11 @@ export component MainWindow inherits Window {
}
SettingsToggleRow { label: UiStrings.daemon; checked <=> root.daemon-enabled; enabled: !root.settings-session-active; divider: false; }
}
if root.service-autostart-platform: Text {
text: root.service-autostart-hint; wrap: word-wrap;
font-size: 11px; color: WorkbenchStyle.muted;
}
}
}
}
@@ -131,9 +131,6 @@ inline int ApplyMainWindowStrings(
strings.set_confirm_delete(
Text(localization::confirm_delete_connection[language]));
strings.set_language(Text(localization::language[language]));
strings.set_capture_method(Text(localization::screen_capture_method[language]));
strings.set_capture_method_auto(
Text(localization::screen_capture_method_auto[language]));
strings.set_codec(Text(localization::video_encode_format[language]));
strings.set_pause_video_when_minimized(
Text(localization::pause_video_when_minimized[language]));