mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-10-27 16:22:11 +08:00
fix: report fatal singleton startup errors
This commit is contained in:
Generated
+1
@@ -1146,6 +1146,7 @@ dependencies = [
|
||||
"regex",
|
||||
"reqwest 0.13.4",
|
||||
"reqwest_dav",
|
||||
"rfd",
|
||||
"runas",
|
||||
"rustls",
|
||||
"scopeguard",
|
||||
|
||||
@@ -60,6 +60,7 @@ bitflags = { workspace = true }
|
||||
socket2 = { workspace = true }
|
||||
warp = { version = "0.4.3", features = ["server"] }
|
||||
open = "5.4.1"
|
||||
rfd = { version = "0.16.0", default-features = false, features = ["common-controls-v6"] }
|
||||
dunce = "1.0.5"
|
||||
nanoid = "0.5"
|
||||
chrono = { workspace = true }
|
||||
|
||||
+55
-7
@@ -31,11 +31,10 @@ mod app_init {
|
||||
use super::*;
|
||||
|
||||
/// Initialize singleton monitoring for other instances
|
||||
pub fn init_singleton_check() -> Result<()> {
|
||||
pub fn init_singleton_check() -> Result<server::SingletonDisposition> {
|
||||
AsyncHandler::block_on(async move {
|
||||
logging!(info, Type::Setup, "开始检查单例实例...");
|
||||
server::check_singleton().await?;
|
||||
Ok(())
|
||||
server::check_singleton().await
|
||||
})
|
||||
}
|
||||
|
||||
@@ -213,17 +212,40 @@ mod app_init {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum StartupAction {
|
||||
Continue,
|
||||
ExitSuccess,
|
||||
ExitFailure,
|
||||
}
|
||||
|
||||
fn handle_singleton_startup(
|
||||
result: Result<server::SingletonDisposition>,
|
||||
report_error: impl FnOnce(&anyhow::Error),
|
||||
) -> StartupAction {
|
||||
match result {
|
||||
Ok(server::SingletonDisposition::Primary) => StartupAction::Continue,
|
||||
Ok(server::SingletonDisposition::Secondary) => StartupAction::ExitSuccess,
|
||||
Err(error) => {
|
||||
report_error(&error);
|
||||
StartupAction::ExitFailure
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run() -> std::process::ExitCode {
|
||||
#[cfg(all(target_os = "macos", not(debug_assertions), not(test), not(feature = "verge-dev")))]
|
||||
if utils::macos_launch_guard::enforce_before_initialization() == utils::macos_launch_guard::LaunchDisposition::Exit
|
||||
{
|
||||
return;
|
||||
return std::process::ExitCode::SUCCESS;
|
||||
}
|
||||
|
||||
let _ = utils::dirs::init_portable_flag();
|
||||
|
||||
if app_init::init_singleton_check().is_err() {
|
||||
return;
|
||||
match handle_singleton_startup(app_init::init_singleton_check(), utils::startup::report_error) {
|
||||
StartupAction::Continue => {}
|
||||
StartupAction::ExitSuccess => return std::process::ExitCode::SUCCESS,
|
||||
StartupAction::ExitFailure => return std::process::ExitCode::FAILURE,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
@@ -483,4 +505,30 @@ pub fn run() {
|
||||
},
|
||||
_ => {}
|
||||
});
|
||||
std::process::ExitCode::SUCCESS
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod startup_tests {
|
||||
use super::{StartupAction, handle_singleton_startup};
|
||||
use crate::utils::server::SingletonDisposition;
|
||||
use std::cell::Cell;
|
||||
|
||||
#[test]
|
||||
fn secondary_instance_exits_quietly_and_successfully() {
|
||||
let reported = Cell::new(false);
|
||||
let action = handle_singleton_startup(Ok(SingletonDisposition::Secondary), |_| reported.set(true));
|
||||
|
||||
assert_eq!(action, StartupAction::ExitSuccess);
|
||||
assert!(!reported.get());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fatal_singleton_error_is_reported_and_fails_startup() {
|
||||
let reported = Cell::new(false);
|
||||
let action = handle_singleton_startup(Err(anyhow::anyhow!("listener failed")), |_| reported.set(true));
|
||||
|
||||
assert_eq!(action, StartupAction::ExitFailure);
|
||||
assert!(reported.get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::{
|
||||
process::ExitCode,
|
||||
sync::atomic::{AtomicUsize, Ordering},
|
||||
};
|
||||
|
||||
fn main() {
|
||||
fn main() -> ExitCode {
|
||||
let default_parallelism = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1);
|
||||
let worker_limit = std::cmp::min(default_parallelism, 8);
|
||||
let blocking_limit = 2 * worker_limit;
|
||||
@@ -25,5 +28,5 @@ fn main() {
|
||||
#[cfg(feature = "tokio-trace")]
|
||||
console_subscriber::init();
|
||||
|
||||
app_lib::run();
|
||||
app_lib::run()
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ pub mod schtasks;
|
||||
pub mod server;
|
||||
pub mod singleton;
|
||||
pub mod speed;
|
||||
pub(crate) mod startup;
|
||||
pub mod tmpl;
|
||||
#[cfg(target_os = "macos")]
|
||||
pub mod tray_speed;
|
||||
|
||||
@@ -42,16 +42,23 @@ static COMMANDS_READY: AtomicBool = AtomicBool::new(false);
|
||||
#[cfg(feature = "verge-dev")]
|
||||
static DEV_QUIT_REQUESTED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
pub async fn check_singleton() -> Result<()> {
|
||||
let record_path = instance_record_path()?;
|
||||
let lock = open_instance_lock(&record_path.with_file_name(INSTANCE_LOCK_FILE))?;
|
||||
if !try_lock_instance(&lock)? {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SingletonDisposition {
|
||||
Primary,
|
||||
Secondary,
|
||||
}
|
||||
|
||||
pub async fn check_singleton() -> Result<SingletonDisposition> {
|
||||
let record_path = instance_record_path().context("failed to resolve singleton instance record path")?;
|
||||
let lock = open_instance_lock(&record_path.with_file_name(INSTANCE_LOCK_FILE))
|
||||
.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);
|
||||
while std::time::Instant::now() < deadline {
|
||||
if let Ok(record) = read_instance_record(&record_path)
|
||||
&& notify_existing_instance(&record).await
|
||||
{
|
||||
bail!("app exists");
|
||||
return Ok(SingletonDisposition::Secondary);
|
||||
}
|
||||
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
@@ -59,19 +66,24 @@ pub async fn check_singleton() -> Result<()> {
|
||||
}
|
||||
|
||||
let preferred = read_instance_record(&record_path).ok().map(|record| record.port);
|
||||
let listener = bind_primary_listener(preferred).await?;
|
||||
let port = listener.local_addr()?.port();
|
||||
let listener = bind_primary_listener(preferred)
|
||||
.await
|
||||
.context("failed to reserve embedded server port")?;
|
||||
let port = listener
|
||||
.local_addr()
|
||||
.context("failed to read embedded server address")?
|
||||
.port();
|
||||
let record = InstanceRecord {
|
||||
port,
|
||||
token: random_token()?,
|
||||
token: random_token().context("failed to generate singleton instance token")?,
|
||||
};
|
||||
write_instance_record(&record_path, &record)?;
|
||||
write_instance_record(&record_path, &record).context("failed to persist singleton instance record")?;
|
||||
INSTANCE_LOCK
|
||||
.set(lock)
|
||||
.map_err(|_| anyhow::anyhow!("singleton lock is already initialized"))?;
|
||||
let _ = EMBEDDED_PORT.set(port);
|
||||
start_embedded_server(listener, record.token);
|
||||
Ok(())
|
||||
Ok(SingletonDisposition::Primary)
|
||||
}
|
||||
|
||||
async fn bind_primary_listener(preferred: Option<u16>) -> Result<tokio::net::TcpListener> {
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
use crate::utils::dirs;
|
||||
use anyhow::{Context as _, Result};
|
||||
use chrono::Local;
|
||||
use std::{
|
||||
fs::OpenOptions,
|
||||
io::Write as _,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
const STARTUP_LOG_FILE: &str = "startup.log";
|
||||
|
||||
pub(crate) fn report_error(error: &anyhow::Error) {
|
||||
let detail = format!("{error:#}");
|
||||
eprintln!("[clash-verge] startup failed: {detail}");
|
||||
|
||||
let log_result = startup_log_path().and_then(|path| {
|
||||
append_error(&path, &detail)?;
|
||||
Ok(path)
|
||||
});
|
||||
let message = match log_result {
|
||||
Ok(path) => format!(
|
||||
"Clash Verge could not start.\n\n{detail}\n\nDiagnostic log:\n{}",
|
||||
path.display()
|
||||
),
|
||||
Err(log_error) => {
|
||||
eprintln!("[clash-verge] failed to write startup log: {log_error:#}");
|
||||
format!(
|
||||
"Clash Verge could not start.\n\n{detail}\n\nThe diagnostic log could not be written:\n{log_error:#}"
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
let _ = rfd::MessageDialog::new()
|
||||
.set_level(rfd::MessageLevel::Error)
|
||||
.set_title("Clash Verge startup failed")
|
||||
.set_description(message)
|
||||
.set_buttons(rfd::MessageButtons::Ok)
|
||||
.show();
|
||||
}
|
||||
|
||||
fn startup_log_path() -> Result<PathBuf> {
|
||||
Ok(dirs::preinit_app_data_dir()?.join("logs").join(STARTUP_LOG_FILE))
|
||||
}
|
||||
|
||||
fn append_error(path: &Path, detail: &str) -> Result<()> {
|
||||
let parent = path.parent().context("startup log has no parent directory")?;
|
||||
std::fs::create_dir_all(parent).context("failed to create startup log directory")?;
|
||||
|
||||
let mut options = OpenOptions::new();
|
||||
options.create(true).append(true);
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::OpenOptionsExt as _;
|
||||
options.mode(0o600);
|
||||
}
|
||||
let mut file = options.open(path).context("failed to open startup log")?;
|
||||
writeln!(
|
||||
file,
|
||||
"[{}] ERROR [Startup] {detail}",
|
||||
Local::now().format("%Y-%m-%d %H:%M:%S%.3f")
|
||||
)
|
||||
.context("failed to write startup log")?;
|
||||
file.flush().context("failed to flush startup log")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::append_error;
|
||||
|
||||
#[test]
|
||||
fn startup_log_is_created_and_appended() -> anyhow::Result<()> {
|
||||
let root = std::env::temp_dir().join(format!("startup-error-log-{}", std::process::id()));
|
||||
let path = root.join("nested/startup.log");
|
||||
|
||||
append_error(&path, "first failure")?;
|
||||
append_error(&path, "second failure")?;
|
||||
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
assert!(content.contains("ERROR [Startup] first failure"));
|
||||
assert!(content.contains("ERROR [Startup] second failure"));
|
||||
std::fs::remove_dir_all(root)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user