From b4fa6dfa732419e7e6a58989fdde297f36956a2d Mon Sep 17 00:00:00 2001 From: dev0 Date: Mon, 27 Apr 2026 02:01:18 -0700 Subject: [PATCH] feat: API improvements for config reload, rule providers, and log stability - Add config_path to GlobalState for config reload support - Add rule provider routes (/providers/rules/*) with list, reload, match endpoints - Wire rule_routes in ApiRunner - Handle lagged broadcast errors in log websocket handler - Add start_idle_listeners/restart_idle to InboundManager to avoid EADDRINUSE on patch - Add get_rule_providers() to Router - Add list_rules()/ruleCount to RuleProvider Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- clash-lib/src/app/api/handlers/config.rs | 96 ++++++++--- clash-lib/src/app/api/handlers/log.rs | 30 ++-- clash-lib/src/app/api/handlers/provider.rs | 154 +++++++++++++++++- clash-lib/src/app/api/runner.rs | 3 +- clash-lib/src/app/inbound/manager.rs | 144 ++++++++++++---- .../providers/rule_provider/mrs.rs | 4 +- .../providers/rule_provider/provider.rs | 51 +++++- clash-lib/src/app/router/mod.rs | 6 + clash-lib/src/lib.rs | 10 +- 9 files changed, 419 insertions(+), 79 deletions(-) diff --git a/clash-lib/src/app/api/handlers/config.rs b/clash-lib/src/app/api/handlers/config.rs index 4fb35cd34..497755d69 100644 --- a/clash-lib/src/app/api/handlers/config.rs +++ b/clash-lib/src/app/api/handlers/config.rs @@ -71,9 +71,9 @@ pub fn routes( async fn get_configs(State(state): State) -> impl IntoResponse { let run_mode = state.dispatcher.get_mode().await; - let log_level = { + let (log_level, config_path) = { let global_state = state.global_state.lock().await; - global_state.log_level + (global_state.log_level, global_state.config_path.clone()) }; let inbound_manager = state.inbound_manager.clone(); @@ -134,6 +134,7 @@ async fn get_configs(State(state): State) -> impl IntoResponse { listeners: Some(listeners), lan_ips, dns_listen, + config_path, }) } @@ -162,8 +163,15 @@ async fn update_configs( let cfg = crate::Config::Str(payload); match g.reload_tx.send((cfg, done)).await { Ok(_) => { - wait.await.unwrap(); - (StatusCode::NO_CONTENT, msg).into_response() + drop(g); + match wait.await { + Ok(_) => (StatusCode::NO_CONTENT, msg).into_response(), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "config reload failed", + ) + .into_response(), + } } Err(_) => ( StatusCode::INTERNAL_SERVER_ERROR, @@ -172,29 +180,64 @@ async fn update_configs( .into_response(), } } - (Some(mut path), None) => { - if !PathBuf::from(&path).is_absolute() { - path = PathBuf::from(g.cwd.clone()) - .join(path) - .to_string_lossy() - .to_string(); + // Empty string path means "reload from the original config file". + (path, None) => { + let resolved = match path.as_deref() { + Some("") | None => { + // Use the path the binary was started with, if available. + match &g.config_path { + Some(p) => p.clone(), + None => { + return ( + StatusCode::BAD_REQUEST, + "no config path provided and no original config \ + file known", + ) + .into_response(); + } + } + } + Some(p) => { + let mut resolved = p.to_string(); + if !PathBuf::from(&resolved).is_absolute() { + resolved = PathBuf::from(g.cwd.clone()) + .join(resolved) + .to_string_lossy() + .to_string(); + } + resolved + } + }; + + if !PathBuf::from(&resolved).exists() { + return ( + StatusCode::BAD_REQUEST, + format!("config file {resolved} not found"), + ) + .into_response(); } - if !PathBuf::from(&path).exists() { + if !PathBuf::from(&resolved).is_file() { return ( StatusCode::BAD_REQUEST, - format!("config file {path} not found"), + format!("{resolved} is not a file"), ) .into_response(); } - let msg = format!("config reloading from file {path}"); - let cfg: crate::Config = crate::Config::File(path); + let msg = format!("config reloading from file {resolved}"); + let cfg: crate::Config = crate::Config::File(resolved); match g.reload_tx.send((cfg, done)).await { Ok(_) => { - wait.await.unwrap(); - (StatusCode::NO_CONTENT, msg).into_response() + drop(g); + match wait.await { + Ok(_) => (StatusCode::NO_CONTENT, msg).into_response(), + Err(_) => ( + StatusCode::INTERNAL_SERVER_ERROR, + "config reload failed", + ) + .into_response(), + } } - Err(_) => ( StatusCode::INTERNAL_SERVER_ERROR, "could not signal config reload", @@ -202,9 +245,6 @@ async fn update_configs( .into_response(), } } - (None, None) => { - (StatusCode::BAD_REQUEST, "no path or payload provided").into_response() - } } } @@ -227,6 +267,8 @@ struct GetConfigResponse { lan_ips: Option>, #[serde(skip_serializing_if = "Option::is_none")] dns_listen: Option, + #[serde(skip_serializing_if = "Option::is_none")] + config_path: Option, } #[derive(Serialize, Deserialize)] @@ -277,6 +319,7 @@ async fn patch_configs( } } + let mut port_changed = false; if payload.rebuild_listeners() { let ports = Ports { port: payload.port, @@ -285,8 +328,8 @@ async fn patch_configs( tproxy_port: payload.tproxy_port, mixed_port: payload.mixed_port, }; - let changed = inbound_manager.change_ports(ports).await; - need_restart |= changed; + port_changed = inbound_manager.change_ports(ports).await; + need_restart |= port_changed; } if let Some(allow_lan) = payload.allow_lan @@ -296,6 +339,7 @@ async fn patch_configs( // TODO: can be done with AtomicBool in each inbound manager, but requires // more changes need_restart = true; + port_changed = false; // force full restart } // Apply mode change before restarting listeners so that new connections @@ -305,7 +349,13 @@ async fn patch_configs( } if need_restart { - let _ = inbound_manager.restart().await; + if port_changed { + // Port-only change: restart only the affected listener(s). + // Unchanged listeners keep running — no EADDRINUSE. + let _ = inbound_manager.restart_idle().await; + } else { + let _ = inbound_manager.restart().await; + } } if let Some(ipv6) = payload.ipv6 { diff --git a/clash-lib/src/app/api/handlers/log.rs b/clash-lib/src/app/api/handlers/log.rs index 45ee3dd88..b2bd593d8 100644 --- a/clash-lib/src/app/api/handlers/log.rs +++ b/clash-lib/src/app/api/handlers/log.rs @@ -19,18 +19,26 @@ pub async fn handle( }) .on_upgrade(move |mut socket| async move { let mut rx = state.log_source_tx.subscribe(); - while let Ok(evt) = rx.recv().await { - let res_str = match serde_json::to_string(&evt) { - Ok(s) => s, - Err(e) => { - warn!("Failed to serialize log event: {}", e); - continue; // Skip this event but keep the connection open + loop { + match rx.recv().await { + Ok(evt) => { + let res_str = match serde_json::to_string(&evt) { + Ok(s) => s, + Err(e) => { + warn!("Failed to serialize log event: {}", e); + continue; + } + }; + if let Err(e) = socket.send(Message::Text(res_str.into())).await + { + warn!("ws send error: {}", e); + break; + } } - }; - - if let Err(e) = socket.send(Message::Text(res_str.into())).await { - warn!("ws send error: {}", e); - break; + Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { + warn!("log ws: receiver lagged {} messages", n); + } + Err(tokio::sync::broadcast::error::RecvError::Closed) => break, } } }) diff --git a/clash-lib/src/app/api/handlers/provider.rs b/clash-lib/src/app/api/handlers/provider.rs index a28b9b669..2efc86d4f 100644 --- a/clash-lib/src/app/api/handlers/provider.rs +++ b/clash-lib/src/app/api/handlers/provider.rs @@ -1,4 +1,10 @@ -use std::{collections::HashMap, sync::Arc, time::Duration}; +use std::{ + collections::HashMap, + net::{IpAddr, Ipv4Addr, SocketAddr}, + str::FromStr, + sync::Arc, + time::Duration, +}; use axum::{ Extension, Router, @@ -8,14 +14,16 @@ use axum::{ response::{IntoResponse, Response}, routing::get, }; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use crate::{ app::{ api::AppState, outbound::manager::ThreadSafeOutboundManager, remote_content_manager::providers::proxy_provider::ThreadSafeProxyProvider, + router::ArcRouter, }, proxy::AnyOutboundHandler, + session::{Network, Session, SocksAddr, Type}, }; #[derive(Clone)] struct ProviderState { @@ -198,3 +206,145 @@ async fn get_proxy_delay( .into_response(), } } + +// ── Rule provider routes ──────────────────────────────────────────────────── + +#[derive(Clone)] +struct RuleProviderState { + router: ArcRouter, +} + +pub fn rule_routes(router: ArcRouter) -> Router> { + let state = RuleProviderState { router }; + Router::new() + .route("/", get(get_rule_providers)) + .route( + "/{provider_name}", + get(get_rule_provider).put(update_rule_provider), + ) + .route("/{provider_name}/rules", get(get_rule_provider_rules)) + .route("/{provider_name}/match", get(match_rule_provider)) + .with_state(state) +} + +async fn get_rule_providers( + State(state): State, +) -> impl IntoResponse { + let mut providers = HashMap::new(); + for (name, p) in state.router.get_rule_providers() { + providers.insert(name.clone(), p.as_map().await); + } + let mut res = HashMap::new(); + res.insert("providers", providers); + axum::response::Json(res) +} + +#[derive(Deserialize)] +struct RuleProviderNamePath { + provider_name: String, +} + +async fn get_rule_provider( + State(state): State, + Path(RuleProviderNamePath { provider_name }): Path, +) -> impl IntoResponse { + match state.router.get_rule_providers().get(&provider_name) { + Some(p) => axum::response::Json(p.as_map().await).into_response(), + None => ( + StatusCode::NOT_FOUND, + format!("rule provider {provider_name} not found"), + ) + .into_response(), + } +} + +async fn update_rule_provider( + State(state): State, + Path(RuleProviderNamePath { provider_name }): Path, +) -> impl IntoResponse { + match state.router.get_rule_providers().get(&provider_name) { + Some(p) => match p.update().await { + Ok(_) => (StatusCode::ACCEPTED, "rule provider update started") + .into_response(), + Err(err) => ( + StatusCode::INTERNAL_SERVER_ERROR, + format!("update rule provider {provider_name} failed: {err}"), + ) + .into_response(), + }, + None => ( + StatusCode::NOT_FOUND, + format!("rule provider {provider_name} not found"), + ) + .into_response(), + } +} + +async fn get_rule_provider_rules( + State(state): State, + Path(RuleProviderNamePath { provider_name }): Path, +) -> impl IntoResponse { + match state.router.get_rule_providers().get(&provider_name) { + Some(p) => { + let rules = p.list_rules(500).await; + let mut res = HashMap::new(); + res.insert("rules", rules); + axum::response::Json(res).into_response() + } + None => ( + StatusCode::NOT_FOUND, + format!("rule provider {provider_name} not found"), + ) + .into_response(), + } +} + +#[derive(Deserialize)] +struct MatchQuery { + target: String, +} + +#[derive(Serialize)] +struct MatchResponse { + #[serde(rename = "match")] + matched: bool, +} + +async fn match_rule_provider( + State(state): State, + Path(RuleProviderNamePath { provider_name }): Path, + Query(q): Query, +) -> impl IntoResponse { + let Some(p) = state.router.get_rule_providers().get(&provider_name) else { + return ( + StatusCode::NOT_FOUND, + format!("rule provider {provider_name} not found"), + ) + .into_response(); + }; + + let destination = match SocksAddr::from_str(&q.target) { + Ok(addr) => addr, + Err(_) => { + return (StatusCode::BAD_REQUEST, "invalid target").into_response(); + } + }; + + let sess = Session { + network: Network::Tcp, + typ: Type::Http, + source: SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0), + destination, + resolved_ip: None, + so_mark: None, + iface: None, + asn: None, + traffic_stats: None, + inbound_user: None, + }; + + axum::response::Json(MatchResponse { + matched: p.search(&sess), + }) + .into_response() +} diff --git a/clash-lib/src/app/api/runner.rs b/clash-lib/src/app/api/runner.rs index fffe4010f..271c0729e 100644 --- a/clash-lib/src/app/api/runner.rs +++ b/clash-lib/src/app/api/runner.rs @@ -158,7 +158,7 @@ impl Runner for ApiRunner { dns_enabled, ), ) - .nest("/rules", handlers::rule::routes(router)) + .nest("/rules", handlers::rule::routes(router.clone())) .nest("/group", handlers::group::routes(outbound_manager.clone())) .nest( "/proxies", @@ -168,6 +168,7 @@ impl Runner for ApiRunner { "/providers/proxies", handlers::provider::routes(outbound_manager), ) + .nest("/providers/rules", handlers::provider::rule_routes(router)) .nest( "/connections", handlers::connection::routes(statistics_manager), diff --git a/clash-lib/src/app/inbound/manager.rs b/clash-lib/src/app/inbound/manager.rs index 64ac861bb..f0b630e73 100644 --- a/clash-lib/src/app/inbound/manager.rs +++ b/clash-lib/src/app/inbound/manager.rs @@ -345,25 +345,83 @@ impl InboundManager { } } - async fn stop_all_listeners(&self) { - for (opt, l) in self.inbound_handlers.write().await.iter_mut() { - if let Some(handler) = l.take() { - warn!("Shutting down inbound handler: {}", opt.common_opts().name); - handler.abort(); + /// Like `start_all_listeners` but skips listeners that already have a + /// running handle. Used after a targeted port change so that only the + /// affected listener is (re-)started. + async fn start_idle_listeners( + dispatcher: Arc, + authenticator: ThreadSafeAuthenticator, + inbound_handlers: Arc>>>>, + cancellation_token: tokio_util::sync::CancellationToken, + ) { + for (opts, handler) in inbound_handlers.write().await.iter_mut() { + if handler.as_ref().is_some_and(|h| !h.is_finished()) { + continue; // already running — do not touch } - *l = None; + let cancellation_token = cancellation_token.clone(); + let name = opts.common_opts().name.clone(); + *handler = build_network_listeners( + opts, + dispatcher.clone(), + authenticator.clone(), + None, + ) + .map(|r| { + tokio::spawn(async move { + tokio::select! { + _ = futures::future::join_all(r) => { + warn!("Inbound handler {} has exited", name); + }, + _ = cancellation_token.cancelled() => { + info!("Inbound handler {} is closed", name); + }, + } + }) + }); } - for handles in self.provider_handles.write().await.values_mut() { - for (opt, entry) in handles.iter_mut() { - if let Some(h) = entry.handle.take() { + } + + async fn stop_all_listeners(&self) { + let mut handles_to_await: Vec> = Vec::new(); + + // Abort static inbound handlers, collect handles for awaiting. + { + let mut guard = self.inbound_handlers.write().await; + for (opt, l) in guard.iter_mut() { + if let Some(handler) = l.take() { warn!( - "Shutting down provider inbound handler: {}", + "Shutting down inbound handler: {}", opt.common_opts().name ); - h.abort(); + handler.abort(); + handles_to_await.push(handler); + } + *l = None; + } + } + + // Abort provider inbound handlers. + { + let mut provider_guard = self.provider_handles.write().await; + for handles in provider_guard.values_mut() { + for (opt, entry) in handles.iter_mut() { + if let Some(h) = entry.handle.take() { + warn!( + "Shutting down provider inbound handler: {}", + opt.common_opts().name + ); + h.abort(); + handles_to_await.push(h); + } } } } + + // Wait for all aborted tasks to finish so the OS ports are released + // before new listeners bind to the same addresses. + for h in handles_to_await { + let _ = h.await; + } } #[allow(dead_code)] @@ -406,6 +464,9 @@ impl InboundManager { } // RESTFUL API handlers below + + /// Full restart — stops ALL listeners then starts them all again. + /// Use for bind-address or allow-lan changes that affect every listener. pub async fn restart(&self) -> Result<(), crate::Error> { self.stop_all_listeners().await; @@ -423,6 +484,24 @@ impl InboundManager { Ok(()) } + /// Partial restart — starts only listeners whose handle is missing or + /// finished. Call this after `change_ports` so that unchanged listeners + /// are not interrupted. + pub async fn restart_idle(&self) -> Result<(), crate::Error> { + let inbound_handlers = self.inbound_handlers.clone(); + let dispatcher = self.dispatcher.clone(); + let authenticator = self.authenticator.clone(); + let cancellation_token = self.cancellation_token.clone(); + Self::start_idle_listeners( + dispatcher, + authenticator, + inbound_handlers, + cancellation_token, + ) + .await; + Ok(()) + } + pub async fn get_ports(&self) -> Ports { let mut ports = Ports::default(); let guard = self.inbound_handlers.read().await; @@ -533,34 +612,27 @@ impl InboundManager { let listeners: HashMap> = guard .extract_if(|opts, _| match &opts { - InboundOpts::Http { common_opts } => { - ports.port.is_some() && Some(common_opts.port) == ports.port - } - InboundOpts::Socks { common_opts, .. } => { - ports.socks_port.is_some() - && Some(common_opts.port) == ports.socks_port - } - InboundOpts::Mixed { common_opts, .. } => { - ports.mixed_port.is_some() - && Some(common_opts.port) == ports.mixed_port - } + InboundOpts::Http { .. } => ports.port.is_some(), + InboundOpts::Socks { .. } => ports.socks_port.is_some(), + InboundOpts::Mixed { .. } => ports.mixed_port.is_some(), #[cfg(feature = "tproxy")] - InboundOpts::TProxy { common_opts, .. } => { - ports.tproxy_port.is_some() - && Some(common_opts.port) == ports.tproxy_port - } + InboundOpts::TProxy { .. } => ports.tproxy_port.is_some(), #[cfg(feature = "redir")] - InboundOpts::Redir { common_opts } => { - ports.redir_port.is_some() - && Some(common_opts.port) == ports.redir_port - } + InboundOpts::Redir { .. } => ports.redir_port.is_some(), _ => false, }) .collect(); let changed = !listeners.is_empty(); + let mut old_handles: Vec> = Vec::new(); + for (mut opts, handle) in listeners { + if let Some(h) = handle { + h.abort(); + old_handles.push(h); + } + // extract_if already guarantees the matching port field is Some. // Use a plain match + if-let (stable) rather than if-let guards // in match arms (which require the nightly `if_let_guard` feature). @@ -588,7 +660,17 @@ impl InboundManager { continue; }; opts.common_opts_mut().port = port; - guard.insert(opts, handle); + // Insert with None so restart_idle will start a new listener. + guard.insert(opts, None); + } + + // Release the write lock before awaiting so other readers aren't blocked. + drop(guard); + + // Wait for the old tasks to finish, which ensures their OS port bindings + // are released before restart_idle binds the new port. + for h in old_handles { + let _ = h.await; } changed diff --git a/clash-lib/src/app/remote_content_manager/providers/rule_provider/mrs.rs b/clash-lib/src/app/remote_content_manager/providers/rule_provider/mrs.rs index 05459c278..25df15c20 100644 --- a/clash-lib/src/app/remote_content_manager/providers/rule_provider/mrs.rs +++ b/clash-lib/src/app/remote_content_manager/providers/rule_provider/mrs.rs @@ -133,7 +133,7 @@ fn parse_domain_payload(reader: &mut R) -> Result { let labels = read_byte_vec(reader, labels_len, "Labels")?; let domain_set = DomainSet::from_mrs_parts(leaves, label_bitmap, labels); - Ok(RuleContent::Domain(domain_set)) + Ok(RuleContent::Domain(domain_set, leaves_len)) } // --- IPCIDR Payload Parsing --- @@ -196,7 +196,7 @@ fn parse_ipcidr_payload(reader: &mut R) -> Result { "Successfully parsed and inserted CIDRs from {} ranges.", ranges_len ); - Ok(RuleContent::Ipcidr(Box::new(cidr_trie))) + Ok(RuleContent::Ipcidr(Box::new(cidr_trie), ranges_len)) } // --- Helper Functions for Reading Data --- diff --git a/clash-lib/src/app/remote_content_manager/providers/rule_provider/provider.rs b/clash-lib/src/app/remote_content_manager/providers/rule_provider/provider.rs index a6c848cb1..fd3166c58 100644 --- a/clash-lib/src/app/remote_content_manager/providers/rule_provider/provider.rs +++ b/clash-lib/src/app/remote_content_manager/providers/rule_provider/provider.rs @@ -77,8 +77,8 @@ impl Display for RuleSetBehavior { pub enum RuleContent { // the left will converted into a right - Domain(succinct_set::DomainSet), - Ipcidr(Box), + Domain(succinct_set::DomainSet, usize), + Ipcidr(Box, usize), Classical(Vec>), } @@ -86,10 +86,18 @@ struct Inner { content: RuleContent, } +#[async_trait] pub trait RuleProvider: Provider { fn search(&self, sess: &Session) -> bool; fn behavior(&self) -> RuleSetBehavior; fn format(&self) -> RuleSetFormat; + /// Returns up to `limit` rules as strings. Only Classical providers return + /// non-empty results; Domain/IPCIDR data structures don't support + /// enumeration. + async fn list_rules(&self, limit: usize) -> Vec { + let _ = limit; + vec![] + } } pub type ThreadSafeRuleProvider = Arc; @@ -127,10 +135,10 @@ impl RuleProviderImpl { let inner = Arc::new(tokio::sync::RwLock::new(Inner { content: match behavior { RuleSetBehavior::Domain => { - RuleContent::Domain(succinct_set::DomainSet::default()) + RuleContent::Domain(succinct_set::DomainSet::default(), 0) } RuleSetBehavior::Ipcidr => { - RuleContent::Ipcidr(Box::new(CidrTrie::new())) + RuleContent::Ipcidr(Box::new(CidrTrie::new()), 0) } RuleSetBehavior::Classical => RuleContent::Classical(vec![]), }, @@ -266,8 +274,8 @@ impl RuleProvider for RuleProviderImpl { match inner { Ok(inner) => match &inner.content { - RuleContent::Domain(set) => set.has(&sess.destination.host()), - RuleContent::Ipcidr(trie) => trie.contains( + RuleContent::Domain(set, _) => set.has(&sess.destination.host()), + RuleContent::Ipcidr(trie, _) => trie.contains( sess.destination .ip() .unwrap_or(IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0))), @@ -295,6 +303,18 @@ impl RuleProvider for RuleProviderImpl { fn format(&self) -> RuleSetFormat { self.format } + + async fn list_rules(&self, limit: usize) -> Vec { + let inner = self.inner.read().await; + match &inner.content { + RuleContent::Classical(rules) => rules + .iter() + .take(limit) + .map(|r| format!("{},{}", r.type_name(), r.payload())) + .collect(), + _ => vec![], + } + } } #[async_trait] @@ -384,6 +404,16 @@ impl Provider for RuleProviderImpl { m.insert("behavior".to_owned(), Box::new(self.behavior().to_string())); m.insert("format".to_owned(), Box::new(self.format().to_string())); + let rule_count = { + let inner = self.inner.read().await; + match &inner.content { + RuleContent::Classical(rules) => rules.len(), + RuleContent::Domain(_, n) => *n, + RuleContent::Ipcidr(_, n) => *n, + } + }; + m.insert("ruleCount".to_owned(), Box::new(rule_count)); + m } } @@ -397,11 +427,16 @@ fn make_rules( ) -> Result { match behavior { RuleSetBehavior::Domain => { + let count = rules.len(); let s = make_domain_rules(rules)?; - Ok(RuleContent::Domain(s.into())) + Ok(RuleContent::Domain(s.into(), count)) } RuleSetBehavior::Ipcidr => { - Ok(RuleContent::Ipcidr(Box::new(make_ip_cidr_rules(rules)?))) + let count = rules.len(); + Ok(RuleContent::Ipcidr( + Box::new(make_ip_cidr_rules(rules)?), + count, + )) } RuleSetBehavior::Classical => Ok(RuleContent::Classical( make_classical_rules(rules, mmdb, geodata)?, diff --git a/clash-lib/src/app/router/mod.rs b/clash-lib/src/app/router/mod.rs index a29d7f00a..087722d65 100644 --- a/clash-lib/src/app/router/mod.rs +++ b/clash-lib/src/app/router/mod.rs @@ -32,6 +32,7 @@ pub struct Router { dns_resolver: ThreadSafeDNSResolver, asn_mmdb: Option, + rule_providers: HashMap, } pub type ArcRouter = Arc; @@ -81,6 +82,7 @@ impl Router { dns_resolver, asn_mmdb, + rule_providers: rule_provider_registry, } } @@ -240,6 +242,10 @@ impl Router { pub fn get_all_rules(&self) -> &Vec> { &self.rules } + + pub fn get_rule_providers(&self) -> &HashMap { + &self.rule_providers + } } pub fn map_rule_type( diff --git a/clash-lib/src/lib.rs b/clash-lib/src/lib.rs index 0e4f7b746..859378e76 100644 --- a/clash-lib/src/lib.rs +++ b/clash-lib/src/lib.rs @@ -123,6 +123,7 @@ pub struct GlobalState { dns_listener: ArcRunner, reload_tx: mpsc::Sender<(Config, oneshot::Sender<()>)>, cwd: String, + config_path: Option, } pub fn start_scaffold(opts: Options) -> Result<()> { @@ -134,6 +135,11 @@ pub fn start_scaffold(opts: Options) -> Result<()> { .enable_all() .build()?, }; + let config_path: Option = if let Config::File(ref p) = opts.config { + Some(p.clone()) + } else { + None + }; let config: InternalConfig = opts.config.try_parse()?; let cwd = opts.cwd.unwrap_or_else(|| ".".to_string()); let (log_tx, _) = broadcast::channel(100); @@ -148,7 +154,7 @@ pub fn start_scaffold(opts: Options) -> Result<()> { ); rt.block_on(async { - match start(config, cwd, log_tx).await { + match start(config, cwd, config_path, log_tx).await { Err(e) => { eprintln!("start error: {e}"); Err(e) @@ -202,6 +208,7 @@ pub fn setup_default_crypto_provider() { pub async fn start( config: InternalConfig, cwd: String, + config_path: Option, log_tx: broadcast::Sender, ) -> Result<()> { setup_default_crypto_provider(); @@ -230,6 +237,7 @@ pub async fn start( dns_listener: components.dns_listener.clone(), reload_tx, cwd: cwd.to_string_lossy().to_string(), + config_path, })); let mut api_listener: ArcRunner = Arc::new(app::api::ApiRunner::new(