mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-11-08 19:22:44 +08:00
refactor(backend): declarative effects (#8128)
* refactor(core): type notice statuses behind the frozen wire contract * refactor(utils): unified retry and strategy-list helpers * refactor(feat): declarative effect registry for verge and clash patches * refactor(core): derive refresh announcements from transaction commit * refactor(core): fold tray, hotkey, autostart updates into effect ensures * refactor(utils): migrate remaining bounded retry loops * docs: add src-tauri architecture guardrails * refactor(core): coalesce burst refresh announcements * refactor(core): complete bounded service retry migration * ci(backend): add architecture ban gates * test(feat): freeze representative effect registry mappings * docs(backend): record dead wire contract entries * chore(core): document watcher exit conditions * fix(backend): preserve applied state across later failures * fix(config): retain runtime after an applied core replacement * refactor(backend): flatten refresh publication and retry policies * docs(backend): focus guardrails on architecture ownership * refactor(effects): use one execution order * fix(config): publish retained runtime and reconcile proxy settings
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::core::notify::NoticeStatus;
|
||||
use crate::{config::with_encryption, process::AsyncHandler};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use clash_verge_logging::{Type, logging};
|
||||
@@ -58,7 +59,7 @@ fn parse_mapping(yaml_str: String, path: PathBuf) -> Result<Mapping> {
|
||||
let error_msg = format!("YAML syntax error in {}: {}", path.display(), err);
|
||||
logging!(error, Type::Config, "{}", error_msg);
|
||||
|
||||
crate::core::handle::Handle::notice_message("config_validate::yaml_syntax_error", &error_msg);
|
||||
crate::core::handle::Handle::notice(NoticeStatus::ConfigValidateYamlSyntaxError, error_msg.as_str());
|
||||
|
||||
bail!("YAML syntax error: {}", err)
|
||||
}
|
||||
|
||||
@@ -109,44 +109,61 @@ fn current_user_home() -> anyhow::Result<PathBuf> {
|
||||
}
|
||||
.max(1024);
|
||||
|
||||
loop {
|
||||
if buffer_size > MAX_BUFFER_SIZE {
|
||||
anyhow::bail!("effective-user home lookup exceeded the maximum buffer size");
|
||||
}
|
||||
use crate::utils::retry::{RetryError, RetryPolicy, retry_sync};
|
||||
retry_sync(
|
||||
RetryPolicy::fixed(
|
||||
std::num::NonZeroUsize::MIN.saturating_add(11),
|
||||
std::time::Duration::ZERO,
|
||||
),
|
||||
|_| {
|
||||
if buffer_size > MAX_BUFFER_SIZE {
|
||||
return Err(RetryError::Stop(anyhow::anyhow!(
|
||||
"effective-user home lookup exceeded the maximum buffer size"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut passwd = std::mem::MaybeUninit::<libc::passwd>::zeroed();
|
||||
let mut result = std::ptr::null_mut();
|
||||
let mut buffer = vec![0_u8; buffer_size];
|
||||
let code = unsafe {
|
||||
libc::getpwuid_r(
|
||||
uid,
|
||||
passwd.as_mut_ptr(),
|
||||
buffer.as_mut_ptr().cast(),
|
||||
buffer.len(),
|
||||
&mut result,
|
||||
)
|
||||
};
|
||||
if code == libc::ERANGE {
|
||||
buffer_size = buffer_size.saturating_mul(2);
|
||||
continue;
|
||||
}
|
||||
if code != 0 {
|
||||
return Err(std::io::Error::from_raw_os_error(code).into());
|
||||
}
|
||||
if result.is_null() {
|
||||
anyhow::bail!("effective user {uid} has no password database entry");
|
||||
}
|
||||
let mut passwd = std::mem::MaybeUninit::<libc::passwd>::zeroed();
|
||||
let mut result = std::ptr::null_mut();
|
||||
let mut buffer = vec![0_u8; buffer_size];
|
||||
let code = unsafe {
|
||||
libc::getpwuid_r(
|
||||
uid,
|
||||
passwd.as_mut_ptr(),
|
||||
buffer.as_mut_ptr().cast(),
|
||||
buffer.len(),
|
||||
&mut result,
|
||||
)
|
||||
};
|
||||
if code == libc::ERANGE {
|
||||
buffer_size = buffer_size.saturating_mul(2);
|
||||
return Err(RetryError::Retry(anyhow::anyhow!(
|
||||
"effective-user home lookup exceeded the maximum buffer size"
|
||||
)));
|
||||
}
|
||||
if code != 0 {
|
||||
return Err(RetryError::Stop(std::io::Error::from_raw_os_error(code).into()));
|
||||
}
|
||||
if result.is_null() {
|
||||
return Err(RetryError::Stop(anyhow::anyhow!(
|
||||
"effective user {uid} has no password database entry"
|
||||
)));
|
||||
}
|
||||
|
||||
let passwd = unsafe { passwd.assume_init() };
|
||||
if passwd.pw_dir.is_null() {
|
||||
anyhow::bail!("effective user {uid} has no home directory");
|
||||
}
|
||||
let home = unsafe { CStr::from_ptr(passwd.pw_dir) };
|
||||
if home.to_bytes().is_empty() {
|
||||
anyhow::bail!("effective user {uid} has an empty home directory");
|
||||
}
|
||||
return Ok(PathBuf::from(std::ffi::OsStr::from_bytes(home.to_bytes())));
|
||||
}
|
||||
let passwd = unsafe { passwd.assume_init() };
|
||||
if passwd.pw_dir.is_null() {
|
||||
return Err(RetryError::Stop(anyhow::anyhow!(
|
||||
"effective user {uid} has no home directory"
|
||||
)));
|
||||
}
|
||||
let home = unsafe { CStr::from_ptr(passwd.pw_dir) };
|
||||
if home.to_bytes().is_empty() {
|
||||
return Err(RetryError::Stop(anyhow::anyhow!(
|
||||
"effective user {uid} has an empty home directory"
|
||||
)));
|
||||
}
|
||||
Ok(PathBuf::from(std::ffi::OsStr::from_bytes(home.to_bytes())))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub const fn move_decision(
|
||||
|
||||
@@ -8,9 +8,10 @@ pub mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod macos_launch_guard;
|
||||
pub(crate) mod network;
|
||||
pub mod notification;
|
||||
pub use crate::core::notify::desktop as notification;
|
||||
pub mod port;
|
||||
pub mod resolve;
|
||||
pub mod retry;
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod schtasks;
|
||||
pub mod server;
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{core::handle, utils::window_manager::WindowState};
|
||||
use clash_verge_i18n;
|
||||
use tauri_plugin_notification::NotificationExt as _;
|
||||
|
||||
pub enum NotificationEvent<'a> {
|
||||
DashboardToggled,
|
||||
ClashModeChanged {
|
||||
mode: &'a str,
|
||||
},
|
||||
SystemProxyToggled(bool),
|
||||
/// A proxy change failed while no window could report it.
|
||||
SystemProxyFailed,
|
||||
/// The system proxy guard stopped.
|
||||
SystemProxyGuardStopped,
|
||||
TunModeToggled(bool),
|
||||
LightweightModeEntered,
|
||||
ProfilesReactivated,
|
||||
AppQuit,
|
||||
}
|
||||
|
||||
/// Whether no app window can currently show the failure.
|
||||
pub const fn needs_system_notification(state: WindowState) -> bool {
|
||||
match state {
|
||||
WindowState::Hidden | WindowState::Minimized | WindowState::NotExist => true,
|
||||
WindowState::VisibleFocused | WindowState::VisibleUnfocused => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn notify(title: Cow<'_, str>, body: Cow<'_, str>) {
|
||||
let app_handle = handle::Handle::app_handle();
|
||||
app_handle.notification().builder().title(title).body(body).show().ok();
|
||||
}
|
||||
|
||||
pub async fn notify_event<'a>(event: NotificationEvent<'a>) {
|
||||
match event {
|
||||
NotificationEvent::DashboardToggled => {
|
||||
let title = clash_verge_i18n::t!("notifications.dashboardToggled.title");
|
||||
let body = clash_verge_i18n::t!("notifications.dashboardToggled.body");
|
||||
notify(title, body);
|
||||
}
|
||||
NotificationEvent::ClashModeChanged { mode } => {
|
||||
let title = clash_verge_i18n::t!("notifications.clashModeChanged.title");
|
||||
let body = clash_verge_i18n::t!("notifications.clashModeChanged.body")
|
||||
.replace("{mode}", mode)
|
||||
.into();
|
||||
notify(title, body);
|
||||
}
|
||||
NotificationEvent::SystemProxyToggled(enabled) => {
|
||||
let title = clash_verge_i18n::t!("notifications.systemProxyToggled.title");
|
||||
let key = if enabled {
|
||||
"notifications.systemProxyToggled.on"
|
||||
} else {
|
||||
"notifications.systemProxyToggled.off"
|
||||
};
|
||||
|
||||
let body = clash_verge_i18n::t!(key);
|
||||
notify(title, body);
|
||||
}
|
||||
NotificationEvent::SystemProxyFailed => {
|
||||
let title = clash_verge_i18n::t!("notifications.systemProxyFailed.title");
|
||||
let body = clash_verge_i18n::t!("notifications.systemProxyFailed.body");
|
||||
notify(title, body);
|
||||
}
|
||||
NotificationEvent::SystemProxyGuardStopped => {
|
||||
let title = clash_verge_i18n::t!("notifications.systemProxyGuardStopped.title");
|
||||
let body = clash_verge_i18n::t!("notifications.systemProxyGuardStopped.body");
|
||||
notify(title, body);
|
||||
}
|
||||
NotificationEvent::TunModeToggled(enabled) => {
|
||||
let title = clash_verge_i18n::t!("notifications.tunModeToggled.title");
|
||||
let key = if enabled {
|
||||
"notifications.tunModeToggled.on"
|
||||
} else {
|
||||
"notifications.tunModeToggled.off"
|
||||
};
|
||||
let body = clash_verge_i18n::t!(key);
|
||||
notify(title, body);
|
||||
}
|
||||
NotificationEvent::LightweightModeEntered => {
|
||||
let title = clash_verge_i18n::t!("notifications.lightweightModeEntered.title");
|
||||
let body = clash_verge_i18n::t!("notifications.lightweightModeEntered.body");
|
||||
notify(title, body);
|
||||
}
|
||||
NotificationEvent::ProfilesReactivated => {
|
||||
let title = clash_verge_i18n::t!("notifications.profilesReactivated.title");
|
||||
let body = clash_verge_i18n::t!("notifications.profilesReactivated.body");
|
||||
notify(title, body);
|
||||
}
|
||||
NotificationEvent::AppQuit => {
|
||||
let title = clash_verge_i18n::t!("notifications.appQuit.title");
|
||||
let body = clash_verge_i18n::t!("notifications.appQuit.body");
|
||||
notify(title, body);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::core::notify::NoticeStatus;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -13,8 +14,8 @@ use crate::{
|
||||
service::{SERVICE_MANAGER, ServiceManager},
|
||||
tray::Tray,
|
||||
},
|
||||
core::{auto_backup::AutoBackupManager, lightweight::auto_lightweight_boot},
|
||||
feat,
|
||||
module::{auto_backup::AutoBackupManager, lightweight::auto_lightweight_boot},
|
||||
process::AsyncHandler,
|
||||
utils::{init, server, window_manager::WindowManager},
|
||||
};
|
||||
@@ -224,7 +225,7 @@ async fn init_core_manager() -> bool {
|
||||
Err(error) => {
|
||||
logging!(error, Type::Setup, "core manager initialization failed: {error:#}");
|
||||
CoreManager::global().record_startup_error(CoreFailure::StartFailed(format!("{error:#}")));
|
||||
Handle::notice_message("core_start::error", "");
|
||||
Handle::notice(NoticeStatus::CoreStartError, "");
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::core::notify::NoticeStatus;
|
||||
use anyhow::Result;
|
||||
use percent_encoding::percent_decode_str;
|
||||
use smartstring::alias::String;
|
||||
@@ -93,18 +94,18 @@ async fn import_subscription(url: &str, name: Option<&String>) {
|
||||
if let Err(e) = profiles::profiles_append_item_safe(&mut item).await {
|
||||
logging!(error, Type::Config, "failed to import subscription url: {:?}", e);
|
||||
Config::profiles().await.discard();
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
handle::Handle::notice(NoticeStatus::ImportSubUrlError, e.to_string());
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = profiles::profiles_save_file_safe().await {
|
||||
logging!(error, Type::Config, "failed to save imported subscription: {e:#}");
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
handle::Handle::notice(NoticeStatus::ImportSubUrlError, e.to_string());
|
||||
return;
|
||||
}
|
||||
logging_error!(Type::Timer, Timer::global().refresh().await);
|
||||
handle::Handle::notice_message(
|
||||
"import_sub_url::ok",
|
||||
handle::Handle::notice(
|
||||
NoticeStatus::ImportSubUrlOk,
|
||||
"", // 空 msg 传入,我们不希望导致 后端-前端-后端 死循环,这里只做提醒。
|
||||
);
|
||||
|
||||
@@ -116,7 +117,7 @@ async fn fetch_profile_item(url: &str, name: Option<&String>) -> Option<PrfItem>
|
||||
Ok(item) => Some(item),
|
||||
Err(e) => {
|
||||
logging!(error, Type::Config, "failed to parse profile from url: {:?}", e);
|
||||
handle::Handle::notice_message("import_sub_url::error", e.to_string());
|
||||
handle::Handle::notice(NoticeStatus::ImportSubUrlError, e.to_string());
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -149,11 +150,11 @@ async fn refresh_core_config() {
|
||||
Ok(outcome) => {
|
||||
let message = outcome.to_string();
|
||||
logging!(warn, Type::Config, "Apply config failed: {}", message);
|
||||
handle::Handle::notice_message("config_validate::error", message);
|
||||
handle::Handle::notice(NoticeStatus::ConfigValidateError, message);
|
||||
}
|
||||
Err(err) => {
|
||||
logging!(error, Type::Config, "Apply config error: {err:#}");
|
||||
handle::Handle::notice_message("update_failed", format!("{err}"));
|
||||
handle::Handle::notice(NoticeStatus::UpdateFailed, format!("{err}"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
use std::{future::Future, num::NonZeroUsize, time::Duration};
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct RetryPolicy {
|
||||
pub attempts: NonZeroUsize,
|
||||
delay: Duration,
|
||||
}
|
||||
|
||||
impl RetryPolicy {
|
||||
pub const fn fixed(attempts: NonZeroUsize, delay: Duration) -> Self {
|
||||
Self { attempts, delay }
|
||||
}
|
||||
|
||||
fn delays(self) -> impl Iterator<Item = Duration> {
|
||||
std::iter::repeat_n(self.delay, self.attempts.get() - 1)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum RetryError<E> {
|
||||
Retry(E),
|
||||
Stop(E),
|
||||
}
|
||||
|
||||
pub async fn retry<T: Send, E: Send, Fut: Future<Output = Result<T, RetryError<E>>> + Send>(
|
||||
policy: RetryPolicy,
|
||||
mut operation: impl FnMut(usize) -> Fut + Send,
|
||||
) -> Result<T, E> {
|
||||
let mut result = operation(0).await;
|
||||
for (index, delay) in policy.delays().enumerate() {
|
||||
match result {
|
||||
Ok(_) | Err(RetryError::Stop(_)) => break,
|
||||
Err(RetryError::Retry(_)) => {
|
||||
if !delay.is_zero() {
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
result = operation(index + 1).await;
|
||||
}
|
||||
result.map_err(|error| match error {
|
||||
RetryError::Retry(error) | RetryError::Stop(error) => error,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn try_strategies<
|
||||
S: Copy + Send,
|
||||
T: Send,
|
||||
E: Send,
|
||||
Fut: Future<Output = Result<T, E>> + Send,
|
||||
const N: usize,
|
||||
>(
|
||||
first: S,
|
||||
rest: [S; N],
|
||||
mut operation: impl FnMut(S) -> Fut + Send,
|
||||
) -> Result<(S, T), E> {
|
||||
retry(
|
||||
RetryPolicy::fixed(NonZeroUsize::MIN.saturating_add(N), Duration::ZERO),
|
||||
move |attempt| {
|
||||
let strategy = if attempt == 0 { first } else { rest[attempt - 1] };
|
||||
let future = operation(strategy);
|
||||
async move { future.await.map(|value| (strategy, value)).map_err(RetryError::Retry) }
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn retry_with_state<S: Send, T: Send, E: Send>(
|
||||
policy: RetryPolicy,
|
||||
mut state: S,
|
||||
mut operation: impl for<'a> FnMut(&'a mut S, usize) -> futures::future::BoxFuture<'a, Result<T, RetryError<E>>> + Send,
|
||||
) -> Result<T, E> {
|
||||
let mut result = operation(&mut state, 0).await;
|
||||
for (index, delay) in policy.delays().enumerate() {
|
||||
match result {
|
||||
Ok(_) | Err(RetryError::Stop(_)) => break,
|
||||
Err(RetryError::Retry(_)) => {
|
||||
if !delay.is_zero() {
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
result = operation(&mut state, index + 1).await;
|
||||
}
|
||||
result.map_err(|error| match error {
|
||||
RetryError::Retry(error) | RetryError::Stop(error) => error,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn retry_sync<T, E>(
|
||||
policy: RetryPolicy,
|
||||
mut operation: impl FnMut(usize) -> Result<T, RetryError<E>>,
|
||||
) -> Result<T, E> {
|
||||
let mut result = operation(0);
|
||||
for (index, delay) in policy.delays().enumerate() {
|
||||
match result {
|
||||
Ok(_) | Err(RetryError::Stop(_)) => break,
|
||||
Err(RetryError::Retry(_)) => {
|
||||
if !delay.is_zero() {
|
||||
std::thread::sleep(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
result = operation(index + 1);
|
||||
}
|
||||
result.map_err(|error| match error {
|
||||
RetryError::Retry(error) | RetryError::Stop(error) => error,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn retries_stop_at_success_terminal_error_or_attempt_limit() {
|
||||
let policy = RetryPolicy::fixed(NonZeroUsize::MIN.saturating_add(2), Duration::ZERO);
|
||||
let mut calls = 0;
|
||||
let success = retry(policy, |attempt| {
|
||||
calls += 1;
|
||||
async move {
|
||||
if attempt == 1 {
|
||||
Ok(7)
|
||||
} else {
|
||||
Err(RetryError::Retry(attempt))
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
assert_eq!((success, calls), (Ok(7), 2));
|
||||
calls = 0;
|
||||
let stopped = retry(policy, |_| {
|
||||
calls += 1;
|
||||
async move { Err::<(), _>(RetryError::Stop("terminal")) }
|
||||
})
|
||||
.await;
|
||||
assert_eq!((stopped, calls), (Err("terminal"), 1));
|
||||
calls = 0;
|
||||
let exhausted = retry(policy, |attempt| {
|
||||
calls += 1;
|
||||
async move { Err::<(), _>(RetryError::Retry(attempt)) }
|
||||
})
|
||||
.await;
|
||||
assert_eq!((exhausted, calls), (Err(2), 3));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn strategies_preserve_order_and_the_last_failure() {
|
||||
let mut visited = Vec::new();
|
||||
let result = try_strategies("direct", ["clash", "system"], |strategy| {
|
||||
visited.push(strategy);
|
||||
async move { Err::<(), _>(strategy) }
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result, Err("system"));
|
||||
assert_eq!(visited, ["direct", "clash", "system"]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stateful_and_sync_retries_preserve_progress_and_stop() {
|
||||
let policy = RetryPolicy::fixed(NonZeroUsize::MIN.saturating_add(2), Duration::ZERO);
|
||||
let result = retry_with_state(policy, Vec::new(), |visited, attempt| {
|
||||
Box::pin(async move {
|
||||
visited.push(attempt);
|
||||
if visited.len() == 3 {
|
||||
Ok(visited.clone())
|
||||
} else {
|
||||
Err(RetryError::Retry(()))
|
||||
}
|
||||
})
|
||||
})
|
||||
.await;
|
||||
assert_eq!(result, Ok(vec![0, 1, 2]));
|
||||
let mut visited = Vec::new();
|
||||
let result = retry_sync(policy, |attempt| {
|
||||
visited.push(attempt);
|
||||
if attempt == 1 {
|
||||
Err::<(), _>(RetryError::Stop("terminal"))
|
||||
} else {
|
||||
Err(RetryError::Retry("retry"))
|
||||
}
|
||||
});
|
||||
assert_eq!(result, Err("terminal"));
|
||||
assert_eq!(visited, [0, 1]);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::resolve;
|
||||
use crate::{
|
||||
config::{Config, DEFAULT_PAC, MixedPort},
|
||||
module::lightweight,
|
||||
core::lightweight,
|
||||
process::AsyncHandler,
|
||||
utils::{dirs, window_manager::WindowManager},
|
||||
};
|
||||
@@ -54,20 +54,34 @@ pub async fn check_singleton() -> Result<SingletonDisposition> {
|
||||
.context("failed to initialize singleton lock")?;
|
||||
if !try_lock_instance(&lock).context("failed to acquire singleton lock")? {
|
||||
let deadline = std::time::Instant::now() + Duration::from_secs(20);
|
||||
loop {
|
||||
if std::time::Instant::now() >= deadline {
|
||||
bail!("another app instance is starting");
|
||||
}
|
||||
// Retry the lock: during restart, the old server stops before releasing it.
|
||||
if try_lock_instance(&lock).context("failed to acquire singleton lock")? {
|
||||
break;
|
||||
}
|
||||
if let Ok(record) = read_instance_record(&record_path)
|
||||
&& notify_existing_instance(&record).await
|
||||
{
|
||||
return Ok(SingletonDisposition::Secondary);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
use crate::utils::retry::{RetryError, RetryPolicy, retry};
|
||||
let secondary = retry(
|
||||
RetryPolicy::fixed(
|
||||
std::num::NonZeroUsize::MIN.saturating_add(400),
|
||||
Duration::from_millis(50),
|
||||
),
|
||||
|_| async {
|
||||
if std::time::Instant::now() >= deadline {
|
||||
return Err(RetryError::Stop(anyhow::anyhow!("another app instance is starting")));
|
||||
}
|
||||
// A restarting primary releases the lock after spawning us, with its server down.
|
||||
if try_lock_instance(&lock)
|
||||
.context("failed to acquire singleton lock")
|
||||
.map_err(RetryError::Stop)?
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
if let Ok(record) = read_instance_record(&record_path)
|
||||
&& notify_existing_instance(&record).await
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
Err(RetryError::Retry(anyhow::anyhow!("another app instance is starting")))
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if secondary {
|
||||
return Ok(SingletonDisposition::Secondary);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user