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 <sunboeasy@gmail.com>

* 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 <sunboeasy@gmail.com>

---------

Signed-off-by: 21pages <sunboeasy@gmail.com>
This commit is contained in:
21pages
2026-02-15 16:12:26 +08:00
committed by GitHub
parent 40f86fa639
commit b268aa1061
2 changed files with 90 additions and 4 deletions

View File

@@ -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<Vec<u8>>,
}
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");
}
}
}

View File

@@ -82,6 +82,10 @@ type ConnMap = HashMap<i32, ConnInner>;
#[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<tokio::sync::oneshot::Sender<()>>) {
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");
}