mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 18:24:36 +00:00
feat(gui): GUI add support to connect to config server (#1596)
This commit is contained in:
@@ -12,6 +12,7 @@ use easytier::rpc_service::remote_client::{
|
|||||||
GetNetworkMetasResponse, ListNetworkInstanceIdsJsonResp, ListNetworkProps, RemoteClientManager,
|
GetNetworkMetasResponse, ListNetworkInstanceIdsJsonResp, ListNetworkProps, RemoteClientManager,
|
||||||
Storage,
|
Storage,
|
||||||
};
|
};
|
||||||
|
use easytier::web_client::{self, WebClient};
|
||||||
use easytier::{
|
use easytier::{
|
||||||
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
|
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
|
||||||
instance_manager::NetworkInstanceManager,
|
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>>>> =
|
static RING_RPC_SERVER: once_cell::sync::Lazy<RwLock<Option<ApiRpcServer<RingTunnelListener>>>> =
|
||||||
once_cell::sync::Lazy::new(|| RwLock::new(None));
|
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 {
|
macro_rules! get_client_manager {
|
||||||
() => {{
|
() => {{
|
||||||
let guard = CLIENT_MANAGER
|
let guard = CLIENT_MANAGER
|
||||||
@@ -93,61 +97,18 @@ async fn run_network_instance(
|
|||||||
cfg: NetworkConfig,
|
cfg: NetworkConfig,
|
||||||
save: bool,
|
save: bool,
|
||||||
) -> Result<(), String> {
|
) -> 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!()?;
|
let client_manager = get_client_manager!()?;
|
||||||
|
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
||||||
#[cfg(target_os = "android")]
|
|
||||||
if cfg.no_tun() == false {
|
|
||||||
client_manager
|
client_manager
|
||||||
.disable_instances_with_tun(&app)
|
.pre_run_network_instance_hook(&app, &toml_config)
|
||||||
.await
|
.await?;
|
||||||
.map_err(|e| e.to_string())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
client_manager
|
client_manager
|
||||||
.handle_run_network_instance(app.clone(), cfg, save)
|
.handle_run_network_instance(app.clone(), cfg, save)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
client_manager
|
||||||
#[cfg(target_os = "android")]
|
.post_run_network_instance_hook(&app, &toml_config.get_id())
|
||||||
if let Some(instance_manager) = INSTANCE_MANAGER.read().await.as_ref() {
|
.await?;
|
||||||
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())?;
|
|
||||||
Ok(())
|
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])
|
.handle_remove_network_instances(app.clone(), vec![instance_id])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,9 +193,13 @@ async fn update_network_config_state(
|
|||||||
.handle_update_network_state(app.clone(), instance_id, disabled)
|
.handle_update_network_state(app.clone(), instance_id, disabled)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
if disabled {
|
if disabled {
|
||||||
client_manager.notify_vpn_stop_if_no_tun(&app)?;
|
client_manager
|
||||||
|
.post_remove_network_instances_hook(&app, &[instance_id])
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -260,13 +228,14 @@ async fn validate_config(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn get_config(app: AppHandle, instance_id: String) -> Result<NetworkConfig, String> {
|
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!()?
|
let cfg = get_client_manager!()?
|
||||||
.storage
|
.handle_get_network_config(app, instance_id)
|
||||||
.get_network_config(app, &instance_id)
|
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?;
|
||||||
.ok_or_else(|| format!("Config not found for instance ID: {}", instance_id))?;
|
Ok(cfg)
|
||||||
Ok(cfg.1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@@ -394,7 +363,7 @@ async fn init_rpc_connection(_app: AppHandle, url: Option<String>) -> Result<(),
|
|||||||
*ring_rpc_server_guard = None;
|
*ring_rpc_server_guard = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut client_manager = tokio::time::timeout(
|
let client_manager = tokio::time::timeout(
|
||||||
std::time::Duration::from_millis(1000),
|
std::time::Duration::from_millis(1000),
|
||||||
manager::GUIClientManager::new(url),
|
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())?
|
.map_err(|_| "connect remote rpc timed out".to_string())?
|
||||||
.with_context(|| "Failed to connect remote rpc")
|
.with_context(|| "Failed to connect remote rpc")
|
||||||
.map_err(|e| format!("{:#}", e))?;
|
.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);
|
*client_manager_guard = Some(client_manager);
|
||||||
|
|
||||||
if !normal_mode {
|
if !normal_mode {
|
||||||
|
drop(WEB_CLIENT.write().await.take());
|
||||||
if let Some(instance_manager) = instance_manager_guard.take() {
|
if let Some(instance_manager) = instance_manager_guard.take() {
|
||||||
instance_manager
|
instance_manager
|
||||||
.retain_network_instance(vec![])
|
.retain_network_instance(vec![])
|
||||||
@@ -424,6 +391,40 @@ async fn is_client_running() -> Result<bool, String> {
|
|||||||
Ok(get_client_manager!()?.rpc_manager.is_running())
|
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"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
fn toggle_window_visibility(app: &tauri::AppHandle) {
|
fn toggle_window_visibility(app: &tauri::AppHandle) {
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
@@ -479,7 +480,7 @@ mod manager {
|
|||||||
use easytier::common::stun::MockStunInfoCollector;
|
use easytier::common::stun::MockStunInfoCollector;
|
||||||
use easytier::launcher::NetworkConfig;
|
use easytier::launcher::NetworkConfig;
|
||||||
use easytier::proto::api::logger::{LoggerRpc, LoggerRpcClientFactory, SetLoggerConfigRequest};
|
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::common::NatType;
|
||||||
use easytier::proto::rpc_impl::bidirect::BidirectRpcManager;
|
use easytier::proto::rpc_impl::bidirect::BidirectRpcManager;
|
||||||
use easytier::proto::rpc_types::controller::BaseController;
|
use easytier::proto::rpc_types::controller::BaseController;
|
||||||
@@ -487,6 +488,38 @@ mod manager {
|
|||||||
use easytier::rpc_service::remote_client::PersistentConfig;
|
use easytier::rpc_service::remote_client::PersistentConfig;
|
||||||
use easytier::tunnel::ring::RingTunnelConnector;
|
use easytier::tunnel::ring::RingTunnelConnector;
|
||||||
use easytier::tunnel::TunnelConnector;
|
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)]
|
#[derive(Clone)]
|
||||||
pub(super) struct GUIConfig(String, pub(crate) NetworkConfig);
|
pub(super) struct GUIConfig(String, pub(crate) NetworkConfig);
|
||||||
@@ -693,6 +726,89 @@ mod manager {
|
|||||||
Ok(())
|
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(
|
fn get_logger_rpc_client(
|
||||||
&self,
|
&self,
|
||||||
) -> Option<Box<dyn LoggerRpc<Controller = BaseController> + Send>> {
|
) -> Option<Box<dyn LoggerRpc<Controller = BaseController> + Send>> {
|
||||||
@@ -737,12 +853,6 @@ mod manager {
|
|||||||
let client = self
|
let client = self
|
||||||
.get_rpc_client(app.clone())
|
.get_rpc_client(app.clone())
|
||||||
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?;
|
.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 {
|
for id in enabled_networks {
|
||||||
if let Ok(uuid) = id.parse() {
|
if let Ok(uuid) = id.parse() {
|
||||||
if !self.storage.enabled_networks.contains(&uuid) {
|
if !self.storage.enabled_networks.contains(&uuid) {
|
||||||
@@ -803,10 +913,11 @@ mod service {
|
|||||||
pub(super) rpc_portal: String,
|
pub(super) rpc_portal: String,
|
||||||
pub(super) file_log_level: String,
|
pub(super) file_log_level: String,
|
||||||
pub(super) file_log_dir: String,
|
pub(super) file_log_dir: String,
|
||||||
|
pub(super) config_server: Option<String>,
|
||||||
}
|
}
|
||||||
impl ServiceOptions {
|
impl ServiceOptions {
|
||||||
fn to_args_vec(&self) -> Vec<std::ffi::OsString> {
|
fn to_args_vec(&self) -> Vec<std::ffi::OsString> {
|
||||||
vec![
|
let mut args = vec![
|
||||||
"--config-dir".into(),
|
"--config-dir".into(),
|
||||||
self.config_dir.clone().into(),
|
self.config_dir.clone().into(),
|
||||||
"--rpc-portal".into(),
|
"--rpc-portal".into(),
|
||||||
@@ -816,7 +927,14 @@ mod service {
|
|||||||
"--file-log-dir".into(),
|
"--file-log-dir".into(),
|
||||||
self.file_log_dir.clone().into(),
|
self.file_log_dir.clone().into(),
|
||||||
"--daemon".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,
|
get_service_status,
|
||||||
init_rpc_connection,
|
init_rpc_connection,
|
||||||
is_client_running,
|
is_client_running,
|
||||||
|
init_web_client,
|
||||||
|
is_web_client_connected,
|
||||||
])
|
])
|
||||||
.on_window_event(|_win, event| match event {
|
.on_window_event(|_win, event| match event {
|
||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
|
|||||||
Vendored
+4
@@ -33,12 +33,14 @@ declare global {
|
|||||||
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
||||||
const initRpcConnection: typeof import('./composables/backend')['initRpcConnection']
|
const initRpcConnection: typeof import('./composables/backend')['initRpcConnection']
|
||||||
const initService: typeof import('./composables/backend')['initService']
|
const initService: typeof import('./composables/backend')['initService']
|
||||||
|
const initWebClient: typeof import('./composables/backend')['initWebClient']
|
||||||
const inject: typeof import('vue')['inject']
|
const inject: typeof import('vue')['inject']
|
||||||
const isClientRunning: typeof import('./composables/backend')['isClientRunning']
|
const isClientRunning: typeof import('./composables/backend')['isClientRunning']
|
||||||
const isProxy: typeof import('vue')['isProxy']
|
const isProxy: typeof import('vue')['isProxy']
|
||||||
const isReactive: typeof import('vue')['isReactive']
|
const isReactive: typeof import('vue')['isReactive']
|
||||||
const isReadonly: typeof import('vue')['isReadonly']
|
const isReadonly: typeof import('vue')['isReadonly']
|
||||||
const isRef: typeof import('vue')['isRef']
|
const isRef: typeof import('vue')['isRef']
|
||||||
|
const isWebClientConnected: typeof import('./composables/backend')['isWebClientConnected']
|
||||||
const listNetworkInstanceIds: typeof import('./composables/backend')['listNetworkInstanceIds']
|
const listNetworkInstanceIds: typeof import('./composables/backend')['listNetworkInstanceIds']
|
||||||
const listenGlobalEvents: typeof import('./composables/event')['listenGlobalEvents']
|
const listenGlobalEvents: typeof import('./composables/event')['listenGlobalEvents']
|
||||||
const loadMode: typeof import('./composables/mode')['loadMode']
|
const loadMode: typeof import('./composables/mode')['loadMode']
|
||||||
@@ -153,12 +155,14 @@ declare module 'vue' {
|
|||||||
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
||||||
readonly initRpcConnection: UnwrapRef<typeof import('./composables/backend')['initRpcConnection']>
|
readonly initRpcConnection: UnwrapRef<typeof import('./composables/backend')['initRpcConnection']>
|
||||||
readonly initService: UnwrapRef<typeof import('./composables/backend')['initService']>
|
readonly initService: UnwrapRef<typeof import('./composables/backend')['initService']>
|
||||||
|
readonly initWebClient: UnwrapRef<typeof import('./composables/backend')['initWebClient']>
|
||||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||||
readonly isClientRunning: UnwrapRef<typeof import('./composables/backend')['isClientRunning']>
|
readonly isClientRunning: UnwrapRef<typeof import('./composables/backend')['isClientRunning']>
|
||||||
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
|
||||||
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
|
||||||
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
|
||||||
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
|
||||||
|
readonly isWebClientConnected: UnwrapRef<typeof import('./composables/backend')['isWebClientConnected']>
|
||||||
readonly listNetworkInstanceIds: UnwrapRef<typeof import('./composables/backend')['listNetworkInstanceIds']>
|
readonly listNetworkInstanceIds: UnwrapRef<typeof import('./composables/backend')['listNetworkInstanceIds']>
|
||||||
readonly listenGlobalEvents: UnwrapRef<typeof import('./composables/event')['listenGlobalEvents']>
|
readonly listenGlobalEvents: UnwrapRef<typeof import('./composables/event')['listenGlobalEvents']>
|
||||||
readonly loadMode: UnwrapRef<typeof import('./composables/mode')['loadMode']>
|
readonly loadMode: UnwrapRef<typeof import('./composables/mode')['loadMode']>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface ServiceOptions {
|
|||||||
rpc_portal: string
|
rpc_portal: string
|
||||||
file_log_level: string
|
file_log_level: string
|
||||||
file_log_dir: string
|
file_log_dir: string
|
||||||
|
config_server?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
|
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
|
||||||
@@ -67,9 +68,9 @@ export async function getConfig(instanceId: string) {
|
|||||||
return await invoke<NetworkConfig>('get_config', { instanceId })
|
return await invoke<NetworkConfig>('get_config', { instanceId })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendConfigs() {
|
export async function sendConfigs(enabledNetworks: string[]) {
|
||||||
let networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]');
|
let networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]');
|
||||||
return await invoke('load_configs', { configs: networkList, enabledNetworks: [] })
|
return await invoke('load_configs', { configs: networkList, enabledNetworks })
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getNetworkMetas(instanceIds: string[]) {
|
export async function getNetworkMetas(instanceIds: string[]) {
|
||||||
@@ -95,3 +96,11 @@ export async function initRpcConnection(url?: string) {
|
|||||||
export async function isClientRunning() {
|
export async function isClientRunning() {
|
||||||
return await invoke<boolean>('is_client_running')
|
return await invoke<boolean>('is_client_running')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function initWebClient(url?: string) {
|
||||||
|
return await invoke('init_web_client', { url })
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function isWebClientConnected() {
|
||||||
|
return await invoke<boolean>('is_web_client_connected')
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
interface NormalMode {
|
import { type } from '@tauri-apps/plugin-os';
|
||||||
|
|
||||||
|
export interface WebClientConfig {
|
||||||
|
config_server_url?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NormalMode extends WebClientConfig {
|
||||||
mode: 'normal'
|
mode: 'normal'
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceMode {
|
export interface ServiceMode extends WebClientConfig {
|
||||||
mode: 'service'
|
mode: 'service'
|
||||||
config_dir: string
|
config_dir: string
|
||||||
rpc_portal: string
|
rpc_portal: string
|
||||||
@@ -19,15 +25,15 @@ export function saveMode(mode: Mode) {
|
|||||||
localStorage.setItem('app_mode', JSON.stringify(mode))
|
localStorage.setItem('app_mode', JSON.stringify(mode))
|
||||||
}
|
}
|
||||||
|
|
||||||
import { type } from '@tauri-apps/plugin-os';
|
|
||||||
|
|
||||||
export function loadMode(): Mode {
|
export function loadMode(): Mode {
|
||||||
if (type() === 'android') {
|
|
||||||
return { mode: 'normal' };
|
|
||||||
}
|
|
||||||
const modeStr = localStorage.getItem('app_mode')
|
const modeStr = localStorage.getItem('app_mode')
|
||||||
if (modeStr) {
|
if (modeStr) {
|
||||||
return JSON.parse(modeStr) as Mode
|
let mode = JSON.parse(modeStr) as Mode
|
||||||
|
if (type() === 'android') {
|
||||||
|
return { ...mode, mode: 'normal' }
|
||||||
|
}
|
||||||
|
return mode
|
||||||
} else {
|
} else {
|
||||||
return { mode: 'normal' }
|
return { mode: 'normal' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,15 +5,15 @@ import { type } from '@tauri-apps/plugin-os'
|
|||||||
import { appLogDir } from '@tauri-apps/api/path'
|
import { appLogDir } from '@tauri-apps/api/path'
|
||||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
|
||||||
import { exit } from '@tauri-apps/plugin-process'
|
import { exit } from '@tauri-apps/plugin-process'
|
||||||
import { I18nUtils, RemoteManagement } from "easytier-frontend-lib"
|
import { I18nUtils, RemoteManagement, Utils } from "easytier-frontend-lib"
|
||||||
import type { MenuItem } from 'primevue/menuitem'
|
import type { MenuItem } from 'primevue/menuitem'
|
||||||
import { useTray } from '~/composables/tray'
|
import { useTray } from '~/composables/tray'
|
||||||
import { GUIRemoteClient } from '~/modules/api'
|
import { GUIRemoteClient } from '~/modules/api'
|
||||||
|
|
||||||
import { useToast, useConfirm } from 'primevue'
|
import { useToast, useConfirm } from 'primevue'
|
||||||
import { loadMode, saveMode, type Mode } from '~/composables/mode'
|
import { loadMode, saveMode, WebClientConfig, type Mode } from '~/composables/mode'
|
||||||
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
||||||
import { getServiceStatus, type ServiceStatus } from '~/composables/backend'
|
import { getServiceStatus } from '~/composables/backend'
|
||||||
|
|
||||||
const { t, locale } = useI18n()
|
const { t, locale } = useI18n()
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
@@ -22,13 +22,13 @@ const modeDialogVisible = ref(false)
|
|||||||
const currentMode = ref<Mode>({ mode: 'normal' })
|
const currentMode = ref<Mode>({ mode: 'normal' })
|
||||||
const editingMode = ref<Mode>({ mode: 'normal' })
|
const editingMode = ref<Mode>({ mode: 'normal' })
|
||||||
const isModeSaving = ref(false)
|
const isModeSaving = ref(false)
|
||||||
const serviceStatus = ref<ServiceStatus>('NotInstalled')
|
const manualDisconnect = ref(false)
|
||||||
|
|
||||||
|
const configServerDialogVisible = ref(false)
|
||||||
|
const configServerConnected = ref(false)
|
||||||
|
|
||||||
async function openModeDialog() {
|
async function openModeDialog() {
|
||||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
|
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
|
||||||
if (editingMode.value.mode === 'service') {
|
|
||||||
serviceStatus.value = await getServiceStatus()
|
|
||||||
}
|
|
||||||
modeDialogVisible.value = true
|
modeDialogVisible.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,6 +84,7 @@ async function onUninstallService() {
|
|||||||
|
|
||||||
async function onStopService() {
|
async function onStopService() {
|
||||||
isModeSaving.value = true
|
isModeSaving.value = true
|
||||||
|
manualDisconnect.value = true
|
||||||
try {
|
try {
|
||||||
await setServiceStatus(false)
|
await setServiceStatus(false)
|
||||||
toast.add({ severity: 'success', summary: t('web.common.success'), detail: t('mode.stop_service_success'), life: 3000 })
|
toast.add({ severity: 'success', summary: t('web.common.success'), detail: t('mode.stop_service_success'), life: 3000 })
|
||||||
@@ -99,11 +100,21 @@ async function onStopService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function initWithMode(mode: Mode) {
|
async function initWithMode(mode: Mode) {
|
||||||
|
const running_inst_ids = (await remoteClient.value.list_network_instance_ids().catch(() => undefined))?.running_inst_ids ?? []
|
||||||
|
|
||||||
if (currentMode.value.mode === 'service' && mode.mode !== 'service') {
|
if (currentMode.value.mode === 'service' && mode.mode !== 'service') {
|
||||||
let serviceStatus = await getServiceStatus()
|
let serviceStatus = await getServiceStatus()
|
||||||
if (serviceStatus === "Running") {
|
if (serviceStatus === "Running") {
|
||||||
|
manualDisconnect.value = true
|
||||||
await setServiceStatus(false)
|
await setServiceStatus(false)
|
||||||
serviceStatus = await getServiceStatus()
|
serviceStatus = await getServiceStatus()
|
||||||
|
for (let i = 0; i < 10; i++) { // macOS takes a while to stop the service
|
||||||
|
if (serviceStatus === "Stopped") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100))
|
||||||
|
serviceStatus = await getServiceStatus()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (serviceStatus === "Stopped") {
|
if (serviceStatus === "Stopped") {
|
||||||
await initService(undefined)
|
await initService(undefined)
|
||||||
@@ -127,11 +138,13 @@ async function initWithMode(mode: Mode) {
|
|||||||
}
|
}
|
||||||
let serviceStatus = await getServiceStatus()
|
let serviceStatus = await getServiceStatus()
|
||||||
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
|
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
|
||||||
|
mode.config_server_url = mode.config_server_url || undefined
|
||||||
await initService({
|
await initService({
|
||||||
config_dir: mode.config_dir,
|
config_dir: mode.config_dir,
|
||||||
file_log_dir: mode.file_log_dir,
|
file_log_dir: mode.file_log_dir,
|
||||||
file_log_level: mode.file_log_level,
|
file_log_level: mode.file_log_level,
|
||||||
rpc_portal: mode.rpc_portal,
|
rpc_portal: mode.rpc_portal,
|
||||||
|
config_server: mode.config_server_url,
|
||||||
})
|
})
|
||||||
serviceStatus = await getServiceStatus()
|
serviceStatus = await getServiceStatus()
|
||||||
}
|
}
|
||||||
@@ -154,6 +167,11 @@ async function initWithMode(mode: Mode) {
|
|||||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await sendConfigs(running_inst_ids.map(Utils.UuidToStr))
|
||||||
|
if (mode.mode === 'normal') {
|
||||||
|
mode.config_server_url = mode.config_server_url || undefined
|
||||||
|
initWebClient(mode.config_server_url)
|
||||||
|
}
|
||||||
currentMode.value = mode
|
currentMode.value = mode
|
||||||
saveMode(mode)
|
saveMode(mode)
|
||||||
clientRunning.value = await isClientRunning()
|
clientRunning.value = await isClientRunning()
|
||||||
@@ -173,6 +191,10 @@ const clientRunning = ref(false);
|
|||||||
|
|
||||||
watch(clientRunning, async (newVal, oldVal) => {
|
watch(clientRunning, async (newVal, oldVal) => {
|
||||||
if (!newVal && oldVal) {
|
if (!newVal && oldVal) {
|
||||||
|
if (manualDisconnect.value) {
|
||||||
|
manualDisconnect.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
await reconnectClient()
|
await reconnectClient()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -187,9 +209,10 @@ onMounted(async () => {
|
|||||||
console.error("Error checking client running status", e)
|
console.error("Error checking client running status", e)
|
||||||
}
|
}
|
||||||
}, 1000)
|
}, 1000)
|
||||||
return () => {
|
|
||||||
|
onUnmounted(() => {
|
||||||
clearInterval(timer)
|
clearInterval(timer)
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
async function reconnectClient() {
|
async function reconnectClient() {
|
||||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()));
|
editingMode.value = JSON.parse(JSON.stringify(loadMode()));
|
||||||
@@ -262,6 +285,12 @@ const setting_menu_items: Ref<MenuItem[]> = ref([
|
|||||||
command: openModeDialog,
|
command: openModeDialog,
|
||||||
visible: () => type() !== 'android',
|
visible: () => type() !== 'android',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: () => `${t('config-server.title')}${t('config-server.' + configServerConnectionStatus.value)}`,
|
||||||
|
icon: 'pi pi-globe',
|
||||||
|
command: openConfigServerDialog,
|
||||||
|
visible: () => ["normal", "service"].includes(currentMode.value.mode),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: 'logging_menu',
|
key: 'logging_menu',
|
||||||
label: () => t('logging'),
|
label: () => t('logging'),
|
||||||
@@ -286,7 +315,6 @@ const setting_menu_items: Ref<MenuItem[]> = ref([
|
|||||||
|
|
||||||
async function connectRpcClient(url?: string) {
|
async function connectRpcClient(url?: string) {
|
||||||
await initRpcConnection(url)
|
await initRpcConnection(url)
|
||||||
await sendConfigs()
|
|
||||||
console.log("easytier rpc connection established")
|
console.log("easytier rpc connection established")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,9 +328,66 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const unlisten = await listenGlobalEvents()
|
const unlisten = await listenGlobalEvents()
|
||||||
return () => {
|
|
||||||
|
onUnmounted(() => {
|
||||||
unlisten()
|
unlisten()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function openConfigServerDialog() {
|
||||||
|
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
|
||||||
|
configServerDialogVisible.value = true
|
||||||
|
}
|
||||||
|
async function onConfigServerSave() {
|
||||||
|
if (JSON.stringify(currentMode.value) === JSON.stringify(editingMode.value)) {
|
||||||
|
configServerDialogVisible.value = false
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
if (editingMode.value.mode === 'service') {
|
||||||
|
await new Promise<void>((resolve, reject) => {
|
||||||
|
confirm.require({
|
||||||
|
message: t('config-server.update_service_confirm'),
|
||||||
|
icon: 'pi pi-exclamation-triangle',
|
||||||
|
rejectProps: {
|
||||||
|
label: t('web.common.cancel'),
|
||||||
|
severity: 'secondary',
|
||||||
|
outlined: true
|
||||||
|
},
|
||||||
|
acceptProps: {
|
||||||
|
label: t('web.common.confirm'),
|
||||||
|
},
|
||||||
|
accept: async () => {
|
||||||
|
resolve()
|
||||||
|
},
|
||||||
|
reject: () => {
|
||||||
|
reject()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
console.log("Saving config server url", (editingMode.value as WebClientConfig).config_server_url)
|
||||||
|
await onModeSave();
|
||||||
|
configServerDialogVisible.value = false
|
||||||
|
}
|
||||||
|
onMounted(() => {
|
||||||
|
const timer = setInterval(async () => {
|
||||||
|
if (currentMode.value.mode !== 'normal') return;
|
||||||
|
if (!currentMode.value.config_server_url) return;
|
||||||
|
configServerConnected.value = await isWebClientConnected();
|
||||||
|
}, 1000)
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearInterval(timer)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
const configServerConnectionStatus = computed(() => {
|
||||||
|
if (currentMode.value.mode !== 'normal') {
|
||||||
|
return 'unknown'
|
||||||
|
}
|
||||||
|
if (!currentMode.value.config_server_url) {
|
||||||
|
return 'disconnected'
|
||||||
|
}
|
||||||
|
return configServerConnected.value ? 'connected' : 'connecting'
|
||||||
})
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -319,10 +404,26 @@ onMounted(async () => {
|
|||||||
<Button :label="t('web.common.save')" icon="pi pi-save" @click="onModeSave" autofocus :loading="isModeSaving" />
|
<Button :label="t('web.common.save')" icon="pi pi-save" @click="onModeSave" autofocus :loading="isModeSaving" />
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="configServerDialogVisible" modal :header="t('config-server.title')"
|
||||||
|
:style="{ width: '50vw' }">
|
||||||
|
<div class="flex flex-col gap-3">
|
||||||
|
<label for="config-server-address">{{ t('config-server.address') }}</label>
|
||||||
|
<InputText id="config-server-address" v-model="(editingMode as WebClientConfig).config_server_url"
|
||||||
|
:placeholder="t('config-server.address_placeholder')" />
|
||||||
|
<small class="p-text-secondary whitespace-pre-wrap">{{ t('config-server.description') }}</small>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="configServerDialogVisible = false" text />
|
||||||
|
<Button :label="t('web.common.save')" icon="pi pi-save" @click="onConfigServerSave" autofocus
|
||||||
|
:loading="isModeSaving" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
<Menu ref="log_menu" :model="log_menu_items_popup" :popup="true" />
|
<Menu ref="log_menu" :model="log_menu_items_popup" :popup="true" />
|
||||||
|
|
||||||
<RemoteManagement v-if="clientRunning" class="flex-1 overflow-y-auto" :api="remoteClient"
|
<RemoteManagement v-if="clientRunning" class="flex-1 overflow-y-auto" :api="remoteClient"
|
||||||
:pause-auto-refresh="isModeSaving" v-bind:instance-id="instanceId" />
|
:pause-auto-refresh="isModeSaving" v-model:instance-id="instanceId" />
|
||||||
<div v-else class="empty-state flex-1 flex flex-col items-center py-12">
|
<div v-else class="empty-state flex-1 flex flex-col items-center py-12">
|
||||||
<i class="pi pi-server text-5xl text-secondary mb-4 opacity-50"></i>
|
<i class="pi pi-server text-5xl text-secondary mb-4 opacity-50"></i>
|
||||||
<div class="text-xl text-center font-medium mb-3">{{ t('client.not_running') }}
|
<div class="text-xl text-center font-medium mb-3">{{ t('client.not_running') }}
|
||||||
@@ -331,7 +432,7 @@ onMounted(async () => {
|
|||||||
iconPos="left" />
|
iconPos="left" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Menubar :model="setting_menu_items" breakpoint="560px">
|
<Menubar :model="setting_menu_items" breakpoint="795px">
|
||||||
<template #item="{ item, props }">
|
<template #item="{ item, props }">
|
||||||
<a v-if="item.key === 'logging_menu'" v-bind="props.action" @click="toggle_log_menu">
|
<a v-if="item.key === 'logging_menu'" v-bind="props.action" @click="toggle_log_menu">
|
||||||
<span :class="item.icon" />
|
<span :class="item.icon" />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
NetworkingMethod,
|
NetworkingMethod,
|
||||||
removeRow
|
removeRow
|
||||||
} from '../types/network'
|
} from '../types/network'
|
||||||
import { defineProps, defineEmits, ref, onMounted } from 'vue'
|
import { defineProps, defineEmits, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -209,11 +209,11 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
resizeObserver.observe(portForwardContainer.value);
|
resizeObserver.observe(portForwardContainer.value);
|
||||||
|
|
||||||
return () => {
|
onUnmounted(() => {
|
||||||
if (resizeObserver && portForwardContainer.value) {
|
if (resizeObserver && portForwardContainer.value) {
|
||||||
resizeObserver.unobserve(portForwardContainer.value);
|
resizeObserver.unobserve(portForwardContainer.value);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -367,6 +367,22 @@ mode:
|
|||||||
remote_rpc_address_empty: 远程RPC地址不能为空
|
remote_rpc_address_empty: 远程RPC地址不能为空
|
||||||
service_config_empty: 服务配置不能为空
|
service_config_empty: 服务配置不能为空
|
||||||
|
|
||||||
|
config-server:
|
||||||
|
title: 配置服务器
|
||||||
|
address: 配置服务器地址
|
||||||
|
address_placeholder: 例如:udp://127.0.0.1:22020/admin 或 admin
|
||||||
|
description: |
|
||||||
|
配置服务器地址,支持以下格式:
|
||||||
|
完整URL:udp://127.0.0.1:22020/admin
|
||||||
|
仅用户名:admin(使用官方服务器)
|
||||||
|
留空:不连接配置服务器
|
||||||
|
connection_status: 连接状态
|
||||||
|
connected: ": 已连接"
|
||||||
|
disconnected: ": 未连接"
|
||||||
|
connecting: ": 连接中..."
|
||||||
|
unknown: ""
|
||||||
|
update_service_confirm: 将重启服务以应用更改,是否继续?
|
||||||
|
|
||||||
client:
|
client:
|
||||||
not_running: 无法连接至远程客户端
|
not_running: 无法连接至远程客户端
|
||||||
retry: 重试
|
retry: 重试
|
||||||
|
|||||||
@@ -367,6 +367,22 @@ mode:
|
|||||||
remote_rpc_address_empty: Remote RPC Address cannot be empty
|
remote_rpc_address_empty: Remote RPC Address cannot be empty
|
||||||
service_config_empty: Service Config cannot be empty
|
service_config_empty: Service Config cannot be empty
|
||||||
|
|
||||||
|
config-server:
|
||||||
|
title: Config Server
|
||||||
|
address: Config Server Address
|
||||||
|
address_placeholder: "e.g.: udp://127.0.0.1:22020/admin or admin"
|
||||||
|
description: |
|
||||||
|
Config server address, allowed formats:
|
||||||
|
Full URL: udp://127.0.0.1:22020/admin
|
||||||
|
Username only: admin (uses official server)
|
||||||
|
Leave blank: Don't connect to a config server
|
||||||
|
connection_status: Connection Status
|
||||||
|
connected: ": Connected"
|
||||||
|
disconnected: ": Disconnected"
|
||||||
|
connecting: ": Connecting..."
|
||||||
|
unknown: ""
|
||||||
|
update_service_confirm: The service will be restarted to apply changes, do you want to continue?
|
||||||
|
|
||||||
client:
|
client:
|
||||||
not_running: Unable to connect to remote client.
|
not_running: Unable to connect to remote client.
|
||||||
retry: Retry
|
retry: Retry
|
||||||
|
|||||||
@@ -311,6 +311,7 @@ mod tests {
|
|||||||
"test",
|
"test",
|
||||||
"test",
|
"test",
|
||||||
Arc::new(NetworkInstanceManager::new()),
|
Arc::new(NetworkInstanceManager::new()),
|
||||||
|
None,
|
||||||
);
|
);
|
||||||
|
|
||||||
wait_for_condition(
|
wait_for_condition(
|
||||||
|
|||||||
@@ -1154,6 +1154,7 @@ async fn run_main(cli: Cli) -> anyhow::Result<()> {
|
|||||||
cli.machine_id.clone(),
|
cli.machine_id.clone(),
|
||||||
cli.network_options.hostname.clone(),
|
cli.network_options.hostname.clone(),
|
||||||
manager.clone(),
|
manager.clone(),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.inspect(|_| {
|
.inspect(|_| {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ use crate::{
|
|||||||
proxy::TcpProxyRpcService, stats::StatsRpcService, vpn_portal::VpnPortalRpcService,
|
proxy::TcpProxyRpcService, stats::StatsRpcService, vpn_portal::VpnPortalRpcService,
|
||||||
},
|
},
|
||||||
tunnel::{tcp::TcpTunnelListener, TunnelListener},
|
tunnel::{tcp::TcpTunnelListener, TunnelListener},
|
||||||
|
web_client::DefaultHooks,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ApiRpcServer<T: TunnelListener + 'static> {
|
pub struct ApiRpcServer<T: TunnelListener + 'static> {
|
||||||
@@ -142,7 +143,10 @@ fn register_api_rpc_service(
|
|||||||
);
|
);
|
||||||
|
|
||||||
registry.register(
|
registry.register(
|
||||||
WebClientServiceServer::new(InstanceManageRpcService::new(instance_manager.clone())),
|
WebClientServiceServer::new(InstanceManageRpcService::new(
|
||||||
|
instance_manager.clone(),
|
||||||
|
Arc::new(DefaultHooks),
|
||||||
|
)),
|
||||||
"",
|
"",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,18 @@ use crate::{
|
|||||||
api::{config::GetConfigRequest, manage::*},
|
api::{config::GetConfigRequest, manage::*},
|
||||||
rpc_types::{self, controller::BaseController},
|
rpc_types::{self, controller::BaseController},
|
||||||
},
|
},
|
||||||
|
web_client::WebClientHooks,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct InstanceManageRpcService {
|
pub struct InstanceManageRpcService {
|
||||||
manager: Arc<NetworkInstanceManager>,
|
manager: Arc<NetworkInstanceManager>,
|
||||||
|
hooks: Arc<dyn WebClientHooks>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InstanceManageRpcService {
|
impl InstanceManageRpcService {
|
||||||
pub fn new(manager: Arc<NetworkInstanceManager>) -> Self {
|
pub fn new(manager: Arc<NetworkInstanceManager>, hooks: Arc<dyn WebClientHooks>) -> Self {
|
||||||
Self { manager }
|
Self { manager, hooks }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +53,14 @@ impl WebClientService for InstanceManageRpcService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut control = if let Some(control) = self.manager.get_instance_config_control(&id) {
|
let mut control = if let Some(control) = self.manager.get_instance_config_control(&id) {
|
||||||
if !req.overwrite {
|
let error_msg = self
|
||||||
|
.manager
|
||||||
|
.get_network_info(&id)
|
||||||
|
.await
|
||||||
|
.and_then(|i| i.error_msg)
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !req.overwrite && error_msg.is_empty() {
|
||||||
return Ok(resp);
|
return Ok(resp);
|
||||||
}
|
}
|
||||||
if control.is_read_only() {
|
if control.is_read_only() {
|
||||||
@@ -96,8 +105,17 @@ impl WebClientService for InstanceManageRpcService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Err(e) = self.hooks.pre_run_network_instance(&cfg).await {
|
||||||
|
return Err(anyhow::anyhow!("pre-run hook failed: {}", e).into());
|
||||||
|
}
|
||||||
|
|
||||||
self.manager.run_network_instance(cfg, true, control)?;
|
self.manager.run_network_instance(cfg, true, control)?;
|
||||||
println!("instance {} started", id);
|
println!("instance {} started", id);
|
||||||
|
|
||||||
|
if let Err(e) = self.hooks.post_run_network_instance(&id).await {
|
||||||
|
tracing::warn!("post-run hook failed: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(resp)
|
Ok(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +191,9 @@ impl WebClientService for InstanceManageRpcService {
|
|||||||
req: DeleteNetworkInstanceRequest,
|
req: DeleteNetworkInstanceRequest,
|
||||||
) -> Result<DeleteNetworkInstanceResponse, rpc_types::error::Error> {
|
) -> Result<DeleteNetworkInstanceResponse, rpc_types::error::Error> {
|
||||||
let inst_ids: HashSet<uuid::Uuid> = req.inst_ids.into_iter().map(Into::into).collect();
|
let inst_ids: HashSet<uuid::Uuid> = req.inst_ids.into_iter().map(Into::into).collect();
|
||||||
|
|
||||||
|
let hook_ids: Vec<uuid::Uuid> = inst_ids.iter().cloned().collect();
|
||||||
|
|
||||||
let inst_ids = self
|
let inst_ids = self
|
||||||
.manager
|
.manager
|
||||||
.iter()
|
.iter()
|
||||||
@@ -190,6 +211,11 @@ impl WebClientService for InstanceManageRpcService {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let remain_inst_ids = self.manager.delete_network_instance(inst_ids)?;
|
let remain_inst_ids = self.manager.delete_network_instance(inst_ids)?;
|
||||||
println!("instance {:?} retained", remain_inst_ids);
|
println!("instance {:?} retained", remain_inst_ids);
|
||||||
|
|
||||||
|
if let Err(e) = self.hooks.post_remove_network_instances(&hook_ids).await {
|
||||||
|
tracing::warn!("post-remove hook failed: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
for config_file in config_files {
|
for config_file in config_files {
|
||||||
if let Err(e) = std::fs::remove_file(&config_file) {
|
if let Err(e) = std::fs::remove_file(&config_file) {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|||||||
@@ -110,11 +110,12 @@ where
|
|||||||
// collect networks that are disabled
|
// collect networks that are disabled
|
||||||
let disabled_inst_ids = self
|
let disabled_inst_ids = self
|
||||||
.get_storage()
|
.get_storage()
|
||||||
.list_network_configs(identify, ListNetworkProps::DisabledOnly)
|
.list_network_configs(identify, ListNetworkProps::All)
|
||||||
.await
|
.await
|
||||||
.map_err(RemoteClientError::PersistentError)?
|
.map_err(RemoteClientError::PersistentError)?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| Into::<crate::proto::common::Uuid>::into(x.get_network_inst_id().to_string()))
|
.map(|x| Into::<crate::proto::common::Uuid>::into(x.get_network_inst_id().to_string()))
|
||||||
|
.filter(|id| !ret.inst_ids.contains(id))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
Ok(ListNetworkInstanceIdsJsonResp {
|
Ok(ListNetworkInstanceIdsJsonResp {
|
||||||
|
|||||||
@@ -2,21 +2,28 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
instance_manager::NetworkInstanceManager,
|
instance_manager::NetworkInstanceManager,
|
||||||
rpc_service::instance_manage::InstanceManageRpcService,
|
rpc_service::instance_manage::InstanceManageRpcService, web_client::WebClientHooks,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Controller {
|
pub struct Controller {
|
||||||
token: String,
|
token: String,
|
||||||
hostname: String,
|
hostname: String,
|
||||||
manager: Arc<NetworkInstanceManager>,
|
manager: Arc<NetworkInstanceManager>,
|
||||||
|
hooks: Arc<dyn WebClientHooks>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Controller {
|
impl Controller {
|
||||||
pub fn new(token: String, hostname: String, manager: Arc<NetworkInstanceManager>) -> Self {
|
pub fn new(
|
||||||
|
token: String,
|
||||||
|
hostname: String,
|
||||||
|
manager: Arc<NetworkInstanceManager>,
|
||||||
|
hooks: Arc<dyn WebClientHooks>,
|
||||||
|
) -> Self {
|
||||||
Controller {
|
Controller {
|
||||||
token,
|
token,
|
||||||
hostname,
|
hostname,
|
||||||
manager,
|
manager,
|
||||||
|
hooks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +40,7 @@ impl Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rpc_service(&self) -> InstanceManageRpcService {
|
pub fn get_rpc_service(&self) -> InstanceManageRpcService {
|
||||||
InstanceManageRpcService::new(self.manager.clone())
|
InstanceManageRpcService::new(self.manager.clone(), self.hooks.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn notify_manager_stopping(&self) {
|
pub(super) fn notify_manager_stopping(&self) {
|
||||||
|
|||||||
@@ -11,15 +11,40 @@ use crate::{
|
|||||||
tunnel::{IpVersion, TunnelConnector},
|
tunnel::{IpVersion, TunnelConnector},
|
||||||
};
|
};
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
|
use async_trait::async_trait;
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait WebClientHooks: Send + Sync {
|
||||||
|
async fn pre_run_network_instance(&self, _cfg: &TomlConfigLoader) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_run_network_instance(&self, _id: &Uuid) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn post_remove_network_instances(&self, _ids: &[Uuid]) -> Result<(), String> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DefaultHooks;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl WebClientHooks for DefaultHooks {}
|
||||||
|
|
||||||
pub mod controller;
|
pub mod controller;
|
||||||
pub mod session;
|
pub mod session;
|
||||||
|
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
|
||||||
pub struct WebClient {
|
pub struct WebClient {
|
||||||
controller: Arc<controller::Controller>,
|
controller: Arc<controller::Controller>,
|
||||||
tasks: ScopedTask<()>,
|
tasks: ScopedTask<()>,
|
||||||
manager_guard: DaemonGuard,
|
manager_guard: DaemonGuard,
|
||||||
|
connected: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebClient {
|
impl WebClient {
|
||||||
@@ -28,28 +53,35 @@ impl WebClient {
|
|||||||
token: S,
|
token: S,
|
||||||
hostname: H,
|
hostname: H,
|
||||||
manager: Arc<NetworkInstanceManager>,
|
manager: Arc<NetworkInstanceManager>,
|
||||||
|
hooks: Option<Arc<dyn WebClientHooks>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let manager_guard = manager.register_daemon();
|
let manager_guard = manager.register_daemon();
|
||||||
|
let hooks = hooks.unwrap_or_else(|| Arc::new(DefaultHooks));
|
||||||
let controller = Arc::new(controller::Controller::new(
|
let controller = Arc::new(controller::Controller::new(
|
||||||
token.to_string(),
|
token.to_string(),
|
||||||
hostname.to_string(),
|
hostname.to_string(),
|
||||||
manager,
|
manager,
|
||||||
|
hooks,
|
||||||
));
|
));
|
||||||
|
let connected = Arc::new(AtomicBool::new(false));
|
||||||
|
|
||||||
let controller_clone = controller.clone();
|
let controller_clone = controller.clone();
|
||||||
|
let connected_clone = connected.clone();
|
||||||
let tasks = ScopedTask::from(tokio::spawn(async move {
|
let tasks = ScopedTask::from(tokio::spawn(async move {
|
||||||
Self::routine(controller_clone, Box::new(connector)).await;
|
Self::routine(controller_clone, connected_clone, Box::new(connector)).await;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
WebClient {
|
WebClient {
|
||||||
controller,
|
controller,
|
||||||
tasks,
|
tasks,
|
||||||
manager_guard,
|
manager_guard,
|
||||||
|
connected,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn routine(
|
async fn routine(
|
||||||
controller: Arc<controller::Controller>,
|
controller: Arc<controller::Controller>,
|
||||||
|
connected: Arc<AtomicBool>,
|
||||||
mut connector: Box<dyn TunnelConnector>,
|
mut connector: Box<dyn TunnelConnector>,
|
||||||
) {
|
) {
|
||||||
loop {
|
loop {
|
||||||
@@ -65,12 +97,18 @@ impl WebClient {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
connected.store(true, Ordering::Release);
|
||||||
println!("Successfully connected to {:?}", conn.info());
|
println!("Successfully connected to {:?}", conn.info());
|
||||||
|
|
||||||
let mut session = session::Session::new(conn, controller.clone());
|
let mut session = session::Session::new(conn, controller.clone());
|
||||||
session.wait().await;
|
session.wait().await;
|
||||||
|
connected.store(false, Ordering::Release);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_connected(&self) -> bool {
|
||||||
|
self.connected.load(Ordering::Acquire)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run_web_client(
|
pub async fn run_web_client(
|
||||||
@@ -78,6 +116,7 @@ pub async fn run_web_client(
|
|||||||
machine_id: Option<String>,
|
machine_id: Option<String>,
|
||||||
hostname: Option<String>,
|
hostname: Option<String>,
|
||||||
manager: Arc<NetworkInstanceManager>,
|
manager: Arc<NetworkInstanceManager>,
|
||||||
|
hooks: Option<Arc<dyn WebClientHooks>>,
|
||||||
) -> Result<WebClient> {
|
) -> Result<WebClient> {
|
||||||
set_default_machine_id(machine_id);
|
set_default_machine_id(machine_id);
|
||||||
let config_server_url = match Url::parse(config_server_url_s) {
|
let config_server_url = match Url::parse(config_server_url_s) {
|
||||||
@@ -87,7 +126,7 @@ pub async fn run_web_client(
|
|||||||
config_server_url_s
|
config_server_url_s
|
||||||
)
|
)
|
||||||
.parse()
|
.parse()
|
||||||
.unwrap(),
|
.with_context(|| "failed to parse config server URL")?,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut c_url = config_server_url.clone();
|
let mut c_url = config_server_url.clone();
|
||||||
@@ -122,6 +161,7 @@ pub async fn run_web_client(
|
|||||||
token.to_string(),
|
token.to_string(),
|
||||||
hostname,
|
hostname,
|
||||||
manager.clone(),
|
manager.clone(),
|
||||||
|
hooks,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,6 +179,7 @@ mod tests {
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
manager.clone(),
|
manager.clone(),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|||||||
Reference in New Issue
Block a user