feat(gui): GUI add support to connect to config server (#1596)

This commit is contained in:
Mg Pig
2025-12-04 23:05:36 +08:00
committed by GitHub
parent 53f279f5ff
commit 0a718163fd
15 changed files with 458 additions and 105 deletions

View File

@@ -12,6 +12,7 @@ use easytier::rpc_service::remote_client::{
GetNetworkMetasResponse, ListNetworkInstanceIdsJsonResp, ListNetworkProps, RemoteClientManager,
Storage,
};
use easytier::web_client::{self, WebClient};
use easytier::{
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
instance_manager::NetworkInstanceManager,
@@ -42,6 +43,9 @@ static CLIENT_MANAGER: once_cell::sync::Lazy<RwLock<Option<manager::GUIClientMan
static RING_RPC_SERVER: once_cell::sync::Lazy<RwLock<Option<ApiRpcServer<RingTunnelListener>>>> =
once_cell::sync::Lazy::new(|| RwLock::new(None));
static WEB_CLIENT: once_cell::sync::Lazy<RwLock<Option<WebClient>>> =
once_cell::sync::Lazy::new(|| RwLock::new(None));
macro_rules! get_client_manager {
() => {{
let guard = CLIENT_MANAGER
@@ -93,61 +97,18 @@ async fn run_network_instance(
cfg: NetworkConfig,
save: bool,
) -> Result<(), String> {
let instance_id = cfg.instance_id().to_string();
app.emit("pre_run_network_instance", cfg.instance_id())
.map_err(|e| e.to_string())?;
let client_manager = get_client_manager!()?;
#[cfg(target_os = "android")]
if cfg.no_tun() == false {
client_manager
.disable_instances_with_tun(&app)
.await
.map_err(|e| e.to_string())?;
}
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
client_manager
.pre_run_network_instance_hook(&app, &toml_config)
.await?;
client_manager
.handle_run_network_instance(app.clone(), cfg, save)
.await
.map_err(|e| e.to_string())?;
#[cfg(target_os = "android")]
if let Some(instance_manager) = INSTANCE_MANAGER.read().await.as_ref() {
let instance_uuid = instance_id
.parse::<uuid::Uuid>()
.map_err(|e| e.to_string())?;
if let Some(instance_ref) = instance_manager
.iter()
.find(|item| *item.key() == instance_uuid)
{
if let Some(mut event_receiver) = instance_ref.value().subscribe_event() {
let app_clone = app.clone();
let instance_id_clone = instance_id.clone();
tokio::spawn(async move {
loop {
match event_receiver.recv().await {
Ok(event) => {
if let easytier::common::global_ctx::GlobalCtxEvent::DhcpIpv4Changed(_, _) = event {
let _ = app_clone.emit("dhcp_ip_changed", instance_id_clone.clone());
}
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break;
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
event_receiver = event_receiver.resubscribe();
}
}
}
});
}
}
}
app.emit("post_run_network_instance", instance_id)
.map_err(|e| e.to_string())?;
client_manager
.post_run_network_instance_hook(&app, &toml_config.get_id())
.await?;
Ok(())
}
@@ -211,7 +172,10 @@ async fn remove_network_instance(app: AppHandle, instance_id: String) -> Result<
.handle_remove_network_instances(app.clone(), vec![instance_id])
.await
.map_err(|e| e.to_string())?;
client_manager.notify_vpn_stop_if_no_tun(&app)?;
client_manager
.post_remove_network_instances_hook(&app, &[instance_id])
.await?;
Ok(())
}
@@ -229,9 +193,13 @@ async fn update_network_config_state(
.handle_update_network_state(app.clone(), instance_id, disabled)
.await
.map_err(|e| e.to_string())?;
if disabled {
client_manager.notify_vpn_stop_if_no_tun(&app)?;
client_manager
.post_remove_network_instances_hook(&app, &[instance_id])
.await?;
}
Ok(())
}
@@ -260,13 +228,14 @@ async fn validate_config(
#[tauri::command]
async fn get_config(app: AppHandle, instance_id: String) -> Result<NetworkConfig, String> {
let instance_id = instance_id
.parse()
.map_err(|e: uuid::Error| e.to_string())?;
let cfg = get_client_manager!()?
.storage
.get_network_config(app, &instance_id)
.handle_get_network_config(app, instance_id)
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("Config not found for instance ID: {}", instance_id))?;
Ok(cfg.1)
.map_err(|e| e.to_string())?;
Ok(cfg)
}
#[tauri::command]
@@ -394,7 +363,7 @@ async fn init_rpc_connection(_app: AppHandle, url: Option<String>) -> Result<(),
*ring_rpc_server_guard = None;
}
let mut client_manager = tokio::time::timeout(
let client_manager = tokio::time::timeout(
std::time::Duration::from_millis(1000),
manager::GUIClientManager::new(url),
)
@@ -402,12 +371,10 @@ async fn init_rpc_connection(_app: AppHandle, url: Option<String>) -> Result<(),
.map_err(|_| "connect remote rpc timed out".to_string())?
.with_context(|| "Failed to connect remote rpc")
.map_err(|e| format!("{:#}", e))?;
if let Some(old_manager) = client_manager_guard.take() {
client_manager.storage = old_manager.storage;
}
*client_manager_guard = Some(client_manager);
if !normal_mode {
drop(WEB_CLIENT.write().await.take());
if let Some(instance_manager) = instance_manager_guard.take() {
instance_manager
.retain_network_instance(vec![])
@@ -424,6 +391,40 @@ async fn is_client_running() -> Result<bool, String> {
Ok(get_client_manager!()?.rpc_manager.is_running())
}
#[tauri::command]
async fn init_web_client(app: AppHandle, url: Option<String>) -> Result<(), String> {
let mut web_client_guard = WEB_CLIENT.write().await;
let Some(url) = url else {
*web_client_guard = None;
return Ok(());
};
let instance_manager = INSTANCE_MANAGER
.try_read()
.map_err(|_| "Failed to acquire read lock for instance manager")?
.clone()
.ok_or_else(|| "Instance manager is not available".to_string())?;
let hooks = Arc::new(manager::GuiHooks { app: app.clone() });
let web_client =
web_client::run_web_client(url.as_str(), None, None, instance_manager, Some(hooks))
.await
.with_context(|| "Failed to initialize web client")
.map_err(|e| format!("{:#}", e))?;
*web_client_guard = Some(web_client);
Ok(())
}
#[tauri::command]
async fn is_web_client_connected() -> Result<bool, String> {
let web_client_guard = WEB_CLIENT.read().await;
if let Some(web_client) = web_client_guard.as_ref() {
Ok(web_client.is_connected())
} else {
Ok(false)
}
}
#[cfg(not(target_os = "android"))]
fn toggle_window_visibility(app: &tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
@@ -479,7 +480,7 @@ mod manager {
use easytier::common::stun::MockStunInfoCollector;
use easytier::launcher::NetworkConfig;
use easytier::proto::api::logger::{LoggerRpc, LoggerRpcClientFactory, SetLoggerConfigRequest};
use easytier::proto::api::manage::{ListNetworkInstanceRequest, RunNetworkInstanceRequest};
use easytier::proto::api::manage::RunNetworkInstanceRequest;
use easytier::proto::common::NatType;
use easytier::proto::rpc_impl::bidirect::BidirectRpcManager;
use easytier::proto::rpc_types::controller::BaseController;
@@ -487,6 +488,38 @@ mod manager {
use easytier::rpc_service::remote_client::PersistentConfig;
use easytier::tunnel::ring::RingTunnelConnector;
use easytier::tunnel::TunnelConnector;
use easytier::web_client::WebClientHooks;
pub(super) struct GuiHooks {
pub(super) app: AppHandle,
}
#[async_trait]
impl WebClientHooks for GuiHooks {
async fn pre_run_network_instance(
&self,
cfg: &easytier::common::config::TomlConfigLoader,
) -> Result<(), String> {
let client_manager = get_client_manager!()?;
client_manager
.pre_run_network_instance_hook(&self.app, cfg)
.await
}
async fn post_run_network_instance(&self, instance_id: &uuid::Uuid) -> Result<(), String> {
let client_manager = get_client_manager!()?;
client_manager
.post_run_network_instance_hook(&self.app, instance_id)
.await
}
async fn post_remove_network_instances(&self, ids: &[uuid::Uuid]) -> Result<(), String> {
let client_manager = get_client_manager!()?;
client_manager
.post_remove_network_instances_hook(&self.app, ids)
.await
}
}
#[derive(Clone)]
pub(super) struct GUIConfig(String, pub(crate) NetworkConfig);
@@ -693,6 +726,89 @@ mod manager {
Ok(())
}
pub(super) async fn pre_run_network_instance_hook(
&self,
app: &AppHandle,
cfg: &easytier::common::config::TomlConfigLoader,
) -> Result<(), String> {
let instance_id = cfg.get_id();
app.emit("pre_run_network_instance", instance_id)
.map_err(|e| e.to_string())?;
#[cfg(target_os = "android")]
if !cfg.get_flags().no_tun {
self.disable_instances_with_tun(app)
.await
.map_err(|e| e.to_string())?;
}
self.storage
.save_config(
app,
instance_id,
NetworkConfig::new_from_config(cfg).map_err(|e| e.to_string())?,
)
.map_err(|e| e.to_string())?;
Ok(())
}
pub(super) async fn post_run_network_instance_hook(
&self,
app: &AppHandle,
instance_id: &uuid::Uuid,
) -> Result<(), String> {
#[cfg(target_os = "android")]
if let Some(instance_manager) = super::INSTANCE_MANAGER.read().await.as_ref() {
let instance_uuid = *instance_id;
if let Some(instance_ref) = instance_manager
.iter()
.find(|item| *item.key() == instance_uuid)
{
if let Some(mut event_receiver) = instance_ref.value().subscribe_event() {
let app_clone = app.clone();
let instance_id_clone = *instance_id;
tokio::spawn(async move {
loop {
match event_receiver.recv().await {
Ok(event) => {
if let easytier::common::global_ctx::GlobalCtxEvent::DhcpIpv4Changed(_, _) = event {
let _ = app_clone.emit("dhcp_ip_changed", instance_id_clone);
}
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
break;
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
event_receiver = event_receiver.resubscribe();
}
}
}
});
}
}
}
self.storage.enabled_networks.insert(*instance_id);
app.emit("post_run_network_instance", instance_id)
.map_err(|e| e.to_string())?;
Ok(())
}
pub(super) async fn post_remove_network_instances_hook(
&self,
app: &AppHandle,
_ids: &[uuid::Uuid],
) -> Result<(), String> {
self.storage
.enabled_networks
.retain(|id| !_ids.contains(id));
self.notify_vpn_stop_if_no_tun(app)?;
Ok(())
}
fn get_logger_rpc_client(
&self,
) -> Option<Box<dyn LoggerRpc<Controller = BaseController> + Send>> {
@@ -737,12 +853,6 @@ mod manager {
let client = self
.get_rpc_client(app.clone())
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?;
let running_instances = client
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
.await?;
for id in running_instances.inst_ids {
self.storage.enabled_networks.insert(id.into());
}
for id in enabled_networks {
if let Ok(uuid) = id.parse() {
if !self.storage.enabled_networks.contains(&uuid) {
@@ -803,10 +913,11 @@ mod service {
pub(super) rpc_portal: String,
pub(super) file_log_level: String,
pub(super) file_log_dir: String,
pub(super) config_server: Option<String>,
}
impl ServiceOptions {
fn to_args_vec(&self) -> Vec<std::ffi::OsString> {
vec![
let mut args = vec![
"--config-dir".into(),
self.config_dir.clone().into(),
"--rpc-portal".into(),
@@ -816,7 +927,14 @@ mod service {
"--file-log-dir".into(),
self.file_log_dir.clone().into(),
"--daemon".into(),
]
];
if let Some(config_server) = &self.config_server {
args.push("--config-server".into());
args.push(config_server.clone().into());
}
args
}
}
@@ -959,6 +1077,8 @@ pub fn run_gui() -> std::process::ExitCode {
get_service_status,
init_rpc_connection,
is_client_running,
init_web_client,
is_web_client_connected,
])
.on_window_event(|_win, event| match event {
#[cfg(not(target_os = "android"))]