From b268aa106188052cf7faf802ff9221c6d028974b Mon Sep 17 00:00:00 2001 From: 21pages Date: Sun, 15 Feb 2026 16:12:26 +0800 Subject: [PATCH] Fix some single device multiple ids scenarios on MacOS (#14196) * fix(macos): sync config to root when root config is empty Signed-off-by: 21pages * fix(server): gate startup on initial config sync; document CheckIfResendPk limitation - wait up to 3s for initial root->local config sync before starting server services - continue startup when timeout is hit, while keeping sync/watch running in background - avoid blocking non-server process startup - clarify that CheckIfResendPk only re-registers PK for current ID and does not solve multi-ID when root uses a non-default mac-generated ID Signed-off-by: 21pages --------- Signed-off-by: 21pages --- src/rendezvous_mediator.rs | 27 +++++++++++++++ src/server.rs | 67 +++++++++++++++++++++++++++++++++++--- 2 files changed, 90 insertions(+), 4 deletions(-) diff --git a/src/rendezvous_mediator.rs b/src/rendezvous_mediator.rs index 5d26d3389..b3ab6a523 100644 --- a/src/rendezvous_mediator.rs +++ b/src/rendezvous_mediator.rs @@ -40,6 +40,7 @@ lazy_static::lazy_static! { } static SHOULD_EXIT: AtomicBool = AtomicBool::new(false); static MANUAL_RESTARTED: AtomicBool = AtomicBool::new(false); +static SENT_REGISTER_PK: AtomicBool = AtomicBool::new(false); #[derive(Clone)] pub struct RendezvousMediator { @@ -689,6 +690,7 @@ impl RendezvousMediator { ..Default::default() }); socket.send(&msg_out).await?; + SENT_REGISTER_PK.store(true, Ordering::SeqCst); Ok(()) } @@ -904,3 +906,28 @@ async fn udp_nat_listen( })?; Ok(()) } + +// When config is not yet synced from root, register_pk may have already been sent with a new generated pk. +// After config sync completes, the pk may change. This struct detects pk changes and triggers +// a re-registration by setting key_confirmed to false. +// NOTE: +// This only corrects PK registration for the current ID. If root uses a non-default mac-generated ID, +// this does not resolve the multi-ID issue by itself. +pub struct CheckIfResendPk { + pk: Option>, +} +impl CheckIfResendPk { + pub fn new() -> Self { + Self { + pk: Config::get_cached_pk(), + } + } +} +impl Drop for CheckIfResendPk { + fn drop(&mut self) { + if SENT_REGISTER_PK.load(Ordering::SeqCst) && Config::get_cached_pk() != self.pk { + Config::set_key_confirmed(false); + log::info!("Set key_confirmed to false due to pk changed, will resend register_pk"); + } + } +} diff --git a/src/server.rs b/src/server.rs index 5dc504fe9..dddc762bf 100644 --- a/src/server.rs +++ b/src/server.rs @@ -82,6 +82,10 @@ type ConnMap = HashMap; #[cfg(any(target_os = "macos", target_os = "linux"))] const CONFIG_SYNC_INTERVAL_SECS: f32 = 0.3; +#[cfg(any(target_os = "macos", target_os = "linux"))] +// 3s is enough for at least one initial sync attempt: +// 0.3s backoff + up to 1s connect timeout + up to 1s response timeout. +const CONFIG_SYNC_INITIAL_WAIT_SECS: u64 = 3; lazy_static::lazy_static! { pub static ref CHILD_PROCESS: Childs = Default::default(); @@ -600,7 +604,7 @@ pub async fn start_server(is_server: bool, no_server: bool) { allow_err!(input_service::setup_uinput(0, 1920, 0, 1080).await); } #[cfg(any(target_os = "macos", target_os = "linux"))] - tokio::spawn(async { sync_and_watch_config_dir().await }); + wait_initial_config_sync().await; #[cfg(target_os = "windows")] crate::platform::try_kill_broker(); #[cfg(feature = "hwcodec")] @@ -685,13 +689,43 @@ pub async fn start_ipc_url_server() { } #[cfg(any(target_os = "macos", target_os = "linux"))] -async fn sync_and_watch_config_dir() { +async fn wait_initial_config_sync() { if crate::platform::is_root() { return; } + // Non-server process should not block startup, but still keeps background sync/watch alive. + if !crate::is_server() { + tokio::spawn(async move { + sync_and_watch_config_dir(None).await; + }); + return; + } + + let (sync_done_tx, mut sync_done_rx) = tokio::sync::oneshot::channel::<()>(); + tokio::spawn(async move { + sync_and_watch_config_dir(Some(sync_done_tx)).await; + }); + + // Server process waits up to N seconds for initial root->local sync to reduce stale-start window. + tokio::select! { + _ = &mut sync_done_rx => { + } + _ = tokio::time::sleep(Duration::from_secs(CONFIG_SYNC_INITIAL_WAIT_SECS)) => { + log::warn!( + "timed out waiting {}s for initial config sync, continue startup and keep syncing in background", + CONFIG_SYNC_INITIAL_WAIT_SECS + ); + } + } +} + +#[cfg(any(target_os = "macos", target_os = "linux"))] +async fn sync_and_watch_config_dir(sync_done_tx: Option>) { let mut cfg0 = (Config::get(), Config2::get()); let mut synced = false; + let mut is_root_config_empty = false; + let mut sync_done_tx = sync_done_tx; let tries = if crate::is_server() { 30 } else { 3 }; log::debug!("#tries of ipc service connection: {}", tries); use hbb_common::sleep; @@ -706,6 +740,8 @@ async fn sync_and_watch_config_dir() { Data::SyncConfig(Some(configs)) => { let (config, config2) = *configs; let _chk = crate::ipc::CheckIfRestart::new(); + #[cfg(target_os = "macos")] + let _chk_pk = crate::CheckIfResendPk::new(); if !config.is_empty() { if cfg0.0 != config { cfg0.0 = config.clone(); @@ -717,8 +753,20 @@ async fn sync_and_watch_config_dir() { Config2::set(config2); log::info!("sync config2 from root"); } + } else { + // only on macos, because this issue was only reproduced on macos + #[cfg(target_os = "macos")] + { + // root config is empty, mark for sync in watch loop + // to prevent root from generating a new config on login screen + is_root_config_empty = true; + } } synced = true; + // Notify startup waiter once initial sync phase finishes successfully. + if let Some(tx) = sync_done_tx.take() { + let _ = tx.send(()); + } } _ => {} }; @@ -729,8 +777,14 @@ async fn sync_and_watch_config_dir() { loop { sleep(CONFIG_SYNC_INTERVAL_SECS).await; let cfg = (Config::get(), Config2::get()); - if cfg != cfg0 { - log::info!("config updated, sync to root"); + let should_sync = + cfg != cfg0 || (is_root_config_empty && !cfg.0.is_empty()); + if should_sync { + if is_root_config_empty { + log::info!("root config is empty, sync our config to root"); + } else { + log::info!("config updated, sync to root"); + } match conn.send(&Data::SyncConfig(Some(cfg.clone().into()))).await { Err(e) => { log::error!("sync config to root failed: {}", e); @@ -745,6 +799,7 @@ async fn sync_and_watch_config_dir() { _ => { cfg0 = cfg; conn.next_timeout(1000).await.ok(); + is_root_config_empty = false; } } } @@ -755,6 +810,10 @@ async fn sync_and_watch_config_dir() { } } } + // Notify startup waiter even when initial sync is skipped/failed, to avoid unnecessary waiting. + if let Some(tx) = sync_done_tx.take() { + let _ = tx.send(()); + } log::warn!("skipped config sync"); }