mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-08-31 08:19:14 +00:00
021f523431
Create easytier-core as the portable owner of configuration, connectivity, tunnels, peer and routing state, gateways, management, the data plane, and instance lifecycle. Keep operating-system integration, native protocol engines, process startup, and presentation in easytier behind explicit Host capability adapters. Create easytier-proto to own schemas, generated RPC types, descriptors, and feature-scoped protocol slices. Remove runtime protobuf reflection from core while preserving unknown route-peer fields across forwarding. Normalize instance construction through CoreInstance, CoreHostAdapters, CoreProcessRuntime, and InstanceManager. Make the runtime config store the only authoritative mutable configuration after startup. Move the portable TCP/UDP data plane into core and extract a generic OperationBroker for completion, cancellation, disposal, and capacity accounting. Expose the session-based FFI v2 completion API and keep the WASI guest ABI, wire schemas, and adapters with core. Migrate CLI, GUI, web, FFI, Android JNI, OHOS, uptime, and mobile consumers to the shared manager and core state. Add explicit user/web config ownership and revision-aware web reconciliation. Preserve configuration, wire, and management behavior while fixing regressions discovered by the full platform and integration matrix: - inherit advertised relay capabilities in foreign networks; - refresh OSPF peer state immediately after runtime config changes; - restore CLI GlobalCtx event output without forcing GUI logging; - retain legacy encryption names and standalone RPC tunnel metadata; - restore ICMP host composition and fragmented UDP handling; - use portable 64-bit atomics on 32-bit MIPS targets; and - retain discarded operations until late cancellation completes. Validate the refactor across 45 GitHub checks, including Linux, macOS, Windows, FreeBSD, web, GUI, Android, OHOS, feature profiles, and three-node and subnet-proxy integration tests. BREAKING CHANGE: internal Rust module paths are not preserved. Legacy native data-plane APIs are replaced by the session-based FFI v2 API. The dedicated Android data-plane wrapper is removed.
1441 lines
48 KiB
Rust
1441 lines
48 KiB
Rust
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
|
|
mod elevate;
|
|
|
|
use anyhow::Context;
|
|
#[cfg(target_os = "android")]
|
|
use easytier::instance::factory::subscribe_native_instance_event;
|
|
use easytier::proto::api::manage::{
|
|
CollectNetworkInfoResponse, ValidateConfigResponse, WebClientService,
|
|
WebClientServiceClientFactory,
|
|
};
|
|
use easytier::web_client::{self, WebClient};
|
|
use easytier::{
|
|
common::config::{NetworkConfig, NetworkConfigExt},
|
|
common::{
|
|
config::{
|
|
ConfigLoader, ConfigSource, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader,
|
|
},
|
|
log,
|
|
},
|
|
instance::factory::{NativeInstanceManager, native_instance_manager},
|
|
proto::rpc::standalone::{runtime_rpc_dialer, runtime_rpc_listener},
|
|
rpc_service::ApiRpcServer,
|
|
utils::panic::setup_panic_handler,
|
|
};
|
|
use easytier_core::management::config_source_to_rpc;
|
|
use easytier_core::management::remote_client::{
|
|
GetNetworkMetasResponse, ListNetworkInstanceIdsJsonResp, ListNetworkProps, RemoteClientManager,
|
|
Storage,
|
|
};
|
|
use easytier_core::{
|
|
connectivity::protocol::raw::TunnelDialer as _, process_runtime::CoreProcessRuntime,
|
|
socket::SocketListener, tunnel::Tunnel,
|
|
};
|
|
use std::ops::Deref;
|
|
use std::sync::Arc;
|
|
use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
|
|
use uuid::Uuid;
|
|
|
|
use tauri::{AppHandle, Emitter, Manager as _};
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
|
|
|
static INSTANCE_MANAGER: once_cell::sync::Lazy<RwLock<Option<Arc<NativeInstanceManager>>>> =
|
|
once_cell::sync::Lazy::new(|| RwLock::new(None));
|
|
|
|
static RPC_RING_UUID: once_cell::sync::Lazy<uuid::Uuid> =
|
|
once_cell::sync::Lazy::new(uuid::Uuid::new_v4);
|
|
|
|
static CLIENT_MANAGER: once_cell::sync::Lazy<RwLock<Option<manager::GUIClientManager>>> =
|
|
once_cell::sync::Lazy::new(|| RwLock::new(None));
|
|
|
|
type BoxedTunnelListener = Box<dyn SocketListener<Accepted = Box<dyn Tunnel>>>;
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum RpcServerKind {
|
|
Ring,
|
|
Tcp,
|
|
}
|
|
|
|
struct RpcServer {
|
|
kind: RpcServerKind,
|
|
_server: ApiRpcServer<BoxedTunnelListener>,
|
|
bind_url: Option<url::Url>,
|
|
}
|
|
static RPC_SERVER: once_cell::sync::Lazy<Mutex<Option<RpcServer>>> =
|
|
once_cell::sync::Lazy::new(|| Mutex::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
|
|
.try_read()
|
|
.map_err(|_| "Failed to acquire read lock for client manager")?;
|
|
RwLockReadGuard::try_map(guard, |cm| cm.as_ref())
|
|
.map_err(|_| "RPC connection not initialized".to_string())
|
|
}};
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn easytier_version() -> Result<String, String> {
|
|
Ok(easytier::VERSION.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn set_dock_visibility(app: tauri::AppHandle, visible: bool) -> Result<(), String> {
|
|
#[cfg(target_os = "macos")]
|
|
{
|
|
use tauri::ActivationPolicy;
|
|
app.set_activation_policy(if visible {
|
|
ActivationPolicy::Regular
|
|
} else {
|
|
ActivationPolicy::Accessory
|
|
})
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
#[cfg(not(target_os = "macos"))]
|
|
let _ = (app, visible);
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
|
|
let toml = cfg.gen_config().map_err(|e| e.to_string())?;
|
|
Ok(toml.dump())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn generate_network_config(toml_config: String) -> Result<NetworkConfig, String> {
|
|
let config = TomlConfigLoader::new_from_str(&toml_config).map_err(|e| e.to_string())?;
|
|
let cfg = NetworkConfig::new_from_config(&config).map_err(|e| e.to_string())?;
|
|
Ok(cfg)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn run_network_instance(
|
|
app: AppHandle,
|
|
cfg: NetworkConfig,
|
|
save: bool,
|
|
) -> Result<(), String> {
|
|
let client_manager = get_client_manager!()?;
|
|
let toml_config = cfg.gen_config().map_err(|e| e.to_string())?;
|
|
client_manager
|
|
.pre_run_network_instance_hook(&app, &toml_config, manager::PersistedConfigSource::User)
|
|
.await?;
|
|
client_manager
|
|
.handle_run_network_instance(app.clone(), cfg, save)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
client_manager
|
|
.post_run_network_instance_hook(&app, &toml_config.get_id())
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn collect_network_info(
|
|
app: AppHandle,
|
|
instance_id: String,
|
|
) -> Result<CollectNetworkInfoResponse, String> {
|
|
let instance_id = instance_id
|
|
.parse()
|
|
.map_err(|e: uuid::Error| e.to_string())?;
|
|
get_client_manager!()?
|
|
.handle_collect_network_info(app, Some(vec![instance_id]))
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn set_logging_level(level: String) -> Result<(), String> {
|
|
get_client_manager!()?
|
|
.set_logging_level(level.clone())
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn set_tun_fd(fd: i32) -> Result<(), String> {
|
|
let Some(instance_manager) = INSTANCE_MANAGER.read().await.clone() else {
|
|
return Err("set_tun_fd is not supported in remote mode".to_string());
|
|
};
|
|
if let Some(uuid) = get_client_manager!()?
|
|
.get_enabled_instances_with_tun_ids()
|
|
.next()
|
|
{
|
|
instance_manager
|
|
.attach_tun_fd(uuid, fd)
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn list_network_instance_ids(
|
|
app: AppHandle,
|
|
) -> Result<ListNetworkInstanceIdsJsonResp, String> {
|
|
get_client_manager!()?
|
|
.handle_list_network_instance_ids(app)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn remove_network_instance(app: AppHandle, instance_id: String) -> Result<(), String> {
|
|
let instance_id = instance_id
|
|
.parse()
|
|
.map_err(|e: uuid::Error| e.to_string())?;
|
|
let client_manager = get_client_manager!()?;
|
|
client_manager
|
|
.handle_remove_network_instances(app.clone(), vec![instance_id])
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
client_manager
|
|
.post_stop_network_instances_hook(&app)
|
|
.await?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn update_network_config_state(
|
|
app: AppHandle,
|
|
instance_id: String,
|
|
disabled: bool,
|
|
) -> Result<(), String> {
|
|
let instance_id = instance_id
|
|
.parse()
|
|
.map_err(|e: uuid::Error| e.to_string())?;
|
|
let client_manager = get_client_manager!()?;
|
|
if !disabled {
|
|
let (cfg, source) = client_manager
|
|
.handle_get_network_config_with_source(app.clone(), instance_id)
|
|
.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,
|
|
manager::PersistedConfigSource::from_runtime_source(source),
|
|
)
|
|
.await?;
|
|
}
|
|
client_manager
|
|
.handle_update_network_state(app.clone(), instance_id, disabled)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
if disabled {
|
|
client_manager
|
|
.post_stop_network_instances_hook(&app)
|
|
.await?;
|
|
} else {
|
|
client_manager
|
|
.post_run_network_instance_hook(&app, &instance_id)
|
|
.await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn save_network_config(app: AppHandle, cfg: NetworkConfig) -> Result<(), String> {
|
|
let instance_id = cfg
|
|
.instance_id()
|
|
.parse()
|
|
.map_err(|e: uuid::Error| e.to_string())?;
|
|
get_client_manager!()?
|
|
.handle_save_network_config(app, instance_id, cfg)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn validate_config(
|
|
app: AppHandle,
|
|
config: NetworkConfig,
|
|
) -> Result<ValidateConfigResponse, String> {
|
|
get_client_manager!()?
|
|
.handle_validate_config(app, config)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[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!()?
|
|
.handle_get_network_config(app, instance_id)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(cfg)
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn load_configs(
|
|
app: AppHandle,
|
|
configs: Vec<manager::StoredGuiConfig>,
|
|
enabled_networks: Vec<String>,
|
|
) -> Result<(), String> {
|
|
get_client_manager!()?
|
|
.load_configs(app, configs, enabled_networks)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn get_network_metas(
|
|
app: AppHandle,
|
|
instance_ids: Vec<uuid::Uuid>,
|
|
) -> Result<GetNetworkMetasResponse, String> {
|
|
get_client_manager!()?
|
|
.handle_get_network_metas(app, instance_ids)
|
|
.await
|
|
.map_err(|e| e.to_string())
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
#[tauri::command]
|
|
fn init_service() -> Result<(), String> {
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
#[tauri::command]
|
|
fn init_service(opts: Option<service::ServiceOptions>) -> Result<(), String> {
|
|
match opts {
|
|
Some(args) => {
|
|
let path = std::path::Path::new(&args.config_dir);
|
|
if !path.exists() {
|
|
std::fs::create_dir_all(&args.config_dir).map_err(|e| e.to_string())?;
|
|
} else if !path.is_dir() {
|
|
return Err("config_dir exists but is not a directory".to_string());
|
|
}
|
|
let path = std::path::Path::new(&args.file_log_dir);
|
|
if !path.exists() {
|
|
std::fs::create_dir_all(&args.file_log_dir).map_err(|e| e.to_string())?;
|
|
} else if !path.is_dir() {
|
|
return Err("file_log_dir exists but is not a directory".to_string());
|
|
}
|
|
|
|
service::install(args).map_err(|e| format!("{:#}", e))?;
|
|
}
|
|
None => {
|
|
service::uninstall().map_err(|e| format!("{:#}", e))?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn set_service_status(_enable: bool) -> Result<(), String> {
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
service::set_status(_enable).map_err(|e| format!("{:#}", e))?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
fn get_service_status() -> Result<&'static str, String> {
|
|
#[cfg(not(target_os = "android"))]
|
|
{
|
|
use easytier::service_manager::ServiceStatus;
|
|
let status = service::status().map_err(|e| format!("{:#}", e))?;
|
|
match status {
|
|
ServiceStatus::NotInstalled => Ok("NotInstalled"),
|
|
ServiceStatus::Stopped(_) => Ok("Stopped"),
|
|
ServiceStatus::Running => Ok("Running"),
|
|
}
|
|
}
|
|
#[cfg(target_os = "android")]
|
|
{
|
|
Ok("NotInstalled")
|
|
}
|
|
}
|
|
|
|
fn normalize_normal_mode_rpc_portal(portal: &str) -> Result<(url::Url, url::Url), String> {
|
|
let portal_url: url::Url = portal
|
|
.parse()
|
|
.map_err(|e| format!("invalid rpc portal: {:#}", e))?;
|
|
let bind_url = portal_url.clone();
|
|
let mut connect_url = portal_url.clone();
|
|
// if bind addr is 0.0.0.0, should convert to 127.0.0.1
|
|
if connect_url.host_str() == Some("0.0.0.0") {
|
|
connect_url.set_host(Some("127.0.0.1")).unwrap();
|
|
}
|
|
Ok((bind_url, connect_url))
|
|
}
|
|
|
|
async fn resolve_rpc_bind_url(url: &url::Url) -> Result<std::net::SocketAddr, String> {
|
|
if url.scheme() != "tcp" {
|
|
return Err(format!("RPC portal requires tcp URL: {url}"));
|
|
}
|
|
let host = url
|
|
.host_str()
|
|
.ok_or_else(|| format!("RPC portal has no host: {url}"))?;
|
|
let port = url.port().unwrap_or(11010);
|
|
tokio::net::lookup_host((host, port))
|
|
.await
|
|
.map_err(|error| format!("failed to resolve RPC portal {url}: {error}"))?
|
|
.next()
|
|
.ok_or_else(|| format!("RPC portal has no resolved address: {url}"))
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn init_rpc_connection(
|
|
_app: AppHandle,
|
|
is_normal_mode: bool,
|
|
url: Option<String>,
|
|
) -> Result<(), String> {
|
|
let mut client_manager_guard =
|
|
tokio::time::timeout(std::time::Duration::from_secs(5), CLIENT_MANAGER.write())
|
|
.await
|
|
.map_err(|_| "Failed to acquire write lock for client manager")?;
|
|
let mut instance_manager_guard = INSTANCE_MANAGER
|
|
.try_write()
|
|
.map_err(|_| "Failed to acquire write lock for instance manager")?;
|
|
let mut rpc_server_guard = RPC_SERVER
|
|
.try_lock()
|
|
.map_err(|_| "Failed to acquire lock for rpc server")?;
|
|
|
|
let mut client_url = url.clone();
|
|
let mut local_process_runtime = None;
|
|
if is_normal_mode {
|
|
let instance_manager = if let Some(im) = instance_manager_guard.take() {
|
|
im
|
|
} else {
|
|
Arc::new(native_instance_manager())
|
|
};
|
|
|
|
let portal = url.and_then(|s| {
|
|
let trimmed = s.trim().to_string();
|
|
if trimmed.is_empty() {
|
|
None
|
|
} else {
|
|
Some(trimmed)
|
|
}
|
|
});
|
|
|
|
let (desired_kind, bind_url, connect_url) = if let Some(portal) = portal {
|
|
let (bind_url, connect_url) = normalize_normal_mode_rpc_portal(&portal)?;
|
|
(RpcServerKind::Tcp, Some(bind_url), Some(connect_url))
|
|
} else {
|
|
(RpcServerKind::Ring, None, None)
|
|
};
|
|
|
|
let need_restart = rpc_server_guard
|
|
.as_ref()
|
|
.map(|x| x.kind != desired_kind || x.bind_url != bind_url)
|
|
.unwrap_or(true);
|
|
|
|
if need_restart {
|
|
*rpc_server_guard = None;
|
|
|
|
let tunnel: BoxedTunnelListener = match desired_kind {
|
|
RpcServerKind::Ring => instance_manager
|
|
.process_runtime()
|
|
.bind_ring_tunnel(*RPC_RING_UUID.deref())
|
|
.map_err(|error| error.to_string())?,
|
|
RpcServerKind::Tcp => {
|
|
let bind_url = bind_url.as_ref().expect("tcp rpc must have bind url");
|
|
Box::new(runtime_rpc_listener(resolve_rpc_bind_url(bind_url).await?))
|
|
}
|
|
};
|
|
|
|
let rpc_server = ApiRpcServer::from_tunnel(tunnel, instance_manager.clone())
|
|
.with_rx_timeout(None)
|
|
.serve()
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
*rpc_server_guard = Some(RpcServer {
|
|
kind: desired_kind,
|
|
_server: rpc_server,
|
|
bind_url,
|
|
});
|
|
}
|
|
|
|
local_process_runtime = Some(instance_manager.process_runtime());
|
|
*instance_manager_guard = Some(instance_manager);
|
|
client_url = connect_url.map(|u| u.to_string());
|
|
} else {
|
|
*rpc_server_guard = None;
|
|
}
|
|
|
|
let client_manager = tokio::time::timeout(
|
|
std::time::Duration::from_millis(1000),
|
|
manager::GUIClientManager::new(client_url, local_process_runtime),
|
|
)
|
|
.await
|
|
.map_err(|_| "connect remote rpc timed out".to_string())?
|
|
.with_context(|| "Failed to connect remote rpc")
|
|
.map_err(|e| format!("{:#}", e))?;
|
|
*client_manager_guard = Some(client_manager);
|
|
|
|
if !is_normal_mode {
|
|
drop(WEB_CLIENT.write().await.take());
|
|
if let Some(instance_manager) = instance_manager_guard.take() {
|
|
instance_manager
|
|
.retain_network_instances(&[])
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
drop(instance_manager);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[tauri::command]
|
|
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 machine_id_state_dir = app
|
|
.path()
|
|
.app_data_dir()
|
|
.with_context(|| "Failed to resolve machine id state directory")
|
|
.map_err(|e| format!("{:#}", e))?;
|
|
|
|
let web_client = web_client::run_web_client(
|
|
url.as_str(),
|
|
easytier::common::MachineIdOptions {
|
|
explicit_machine_id: None,
|
|
state_dir: Some(machine_id_state_dir),
|
|
},
|
|
None,
|
|
false,
|
|
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)
|
|
}
|
|
}
|
|
|
|
// 获取日志目录的辅助函数
|
|
fn get_log_dir(app: &tauri::AppHandle) -> Result<std::path::PathBuf, tauri::Error> {
|
|
if cfg!(target_os = "android") {
|
|
// Android: cache_dir + logs 子目录
|
|
app.path().cache_dir().map(|p| p.join("logs"))
|
|
} else {
|
|
// 其他平台: 默认日志目录
|
|
app.path().app_log_dir()
|
|
}
|
|
}
|
|
|
|
#[tauri::command]
|
|
async fn get_log_dir_path(app: tauri::AppHandle) -> Result<String, String> {
|
|
match get_log_dir(&app) {
|
|
Ok(log_dir) => {
|
|
std::fs::create_dir_all(&log_dir).ok();
|
|
Ok(log_dir.to_string_lossy().to_string())
|
|
}
|
|
Err(e) => Err(format!("Failed to get log directory: {}", e)),
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
fn toggle_window_visibility(app: &tauri::AppHandle) {
|
|
if let Some(window) = app.get_webview_window("main") {
|
|
let visible = window.is_visible().unwrap_or_default();
|
|
let minimized = window.is_minimized().unwrap_or_default();
|
|
let focused = window.is_focused().unwrap_or_default();
|
|
|
|
let should_show = !visible || minimized || !focused;
|
|
if should_show {
|
|
if !visible {
|
|
let _ = window.show();
|
|
}
|
|
if minimized {
|
|
let _ = window.unminimize();
|
|
}
|
|
if !focused {
|
|
let _ = window.set_focus();
|
|
}
|
|
let _ = set_dock_visibility(app.clone(), true);
|
|
} else {
|
|
let _ = window.hide();
|
|
let _ = set_dock_visibility(app.clone(), false);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn get_exe_path() -> String {
|
|
if let Ok(appimage_path) = std::env::var("APPIMAGE")
|
|
&& !appimage_path.is_empty()
|
|
{
|
|
return appimage_path;
|
|
}
|
|
std::env::current_exe()
|
|
.map(|p| p.to_string_lossy().to_string())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
fn check_sudo() -> bool {
|
|
let is_elevated = elevate::Command::is_elevated();
|
|
if !is_elevated {
|
|
let exe_path = get_exe_path();
|
|
let stdcmd = std::process::Command::new(&exe_path);
|
|
elevate::Command::new(stdcmd)
|
|
.output()
|
|
.expect("Failed to run elevated command");
|
|
}
|
|
is_elevated
|
|
}
|
|
|
|
mod manager {
|
|
use super::*;
|
|
use async_trait::async_trait;
|
|
use dashmap::{DashMap, DashSet};
|
|
use easytier::common::config::{NetworkConfig, NetworkConfigExt};
|
|
use easytier::proto::api::logger::{LoggerRpc, LoggerRpcClientFactory, SetLoggerConfigRequest};
|
|
use easytier::proto::api::manage::RunNetworkInstanceRequest;
|
|
use easytier::proto::rpc::bidirect::BidirectRpcManager;
|
|
use easytier::proto::rpc_types::controller::BaseController;
|
|
use easytier::web_client::WebClientHooks;
|
|
use easytier_core::management::remote_client::PersistentConfig;
|
|
|
|
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,
|
|
PersistedConfigSource::from_runtime_source(cfg.get_network_config_source()),
|
|
)
|
|
.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_remote_remove_network_instances_hook(&self.app, ids)
|
|
.await
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
#[derive(Default)]
|
|
pub(super) enum PersistedConfigSource {
|
|
User,
|
|
#[serde(alias = "webhook")]
|
|
Web,
|
|
#[serde(other)]
|
|
#[default]
|
|
Legacy,
|
|
}
|
|
|
|
impl PersistedConfigSource {
|
|
pub(super) fn from_runtime_source(source: ConfigSource) -> Self {
|
|
match source {
|
|
ConfigSource::User => Self::User,
|
|
ConfigSource::Web => Self::Web,
|
|
}
|
|
}
|
|
|
|
fn merge_persisted(self, incoming: Self) -> Self {
|
|
match (self, incoming) {
|
|
// Older runtimes report missing source as `user`. Keep the stronger persisted
|
|
// ownership until web sync or an explicit user save repairs it.
|
|
(Self::Web, Self::User) | (Self::Legacy, Self::User) => self,
|
|
(_, next) => next,
|
|
}
|
|
}
|
|
|
|
fn to_runtime_source(self) -> ConfigSource {
|
|
match self {
|
|
Self::User | Self::Legacy => ConfigSource::User,
|
|
Self::Web => ConfigSource::Web,
|
|
}
|
|
}
|
|
|
|
#[cfg(any(test, target_os = "android"))]
|
|
fn is_web_like(self) -> bool {
|
|
matches!(self, Self::Web)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
pub(super) struct GUIConfig {
|
|
inst_id: String,
|
|
pub(crate) config: NetworkConfig,
|
|
source: PersistedConfigSource,
|
|
}
|
|
|
|
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
|
pub(super) struct StoredGuiConfig {
|
|
config: NetworkConfig,
|
|
#[serde(default)]
|
|
source: PersistedConfigSource,
|
|
}
|
|
|
|
impl GUIConfig {
|
|
fn new(inst_id: String, config: NetworkConfig, source: PersistedConfigSource) -> Self {
|
|
Self {
|
|
inst_id,
|
|
config,
|
|
source,
|
|
}
|
|
}
|
|
|
|
fn into_stored(self) -> StoredGuiConfig {
|
|
StoredGuiConfig {
|
|
config: self.config,
|
|
source: self.source,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PersistentConfig<anyhow::Error> for GUIConfig {
|
|
fn get_network_inst_id(&self) -> &str {
|
|
&self.inst_id
|
|
}
|
|
fn get_network_config(&self) -> Result<NetworkConfig, anyhow::Error> {
|
|
Ok(self.config.clone())
|
|
}
|
|
fn get_network_config_source(&self) -> ConfigSource {
|
|
self.source.to_runtime_source()
|
|
}
|
|
}
|
|
|
|
pub(super) struct GUIStorage {
|
|
network_configs: DashMap<Uuid, GUIConfig>,
|
|
enabled_networks: DashSet<Uuid>,
|
|
}
|
|
impl GUIStorage {
|
|
fn new() -> Self {
|
|
Self {
|
|
network_configs: DashMap::new(),
|
|
enabled_networks: DashSet::new(),
|
|
}
|
|
}
|
|
|
|
fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> {
|
|
let configs = self
|
|
.network_configs
|
|
.iter()
|
|
.map(|entry| entry.value().clone().into_stored())
|
|
.collect::<Vec<_>>();
|
|
app.emit("save_configs", configs)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn save_enabled_networks(&self, app: &AppHandle) -> anyhow::Result<()> {
|
|
let payload: Vec<String> = self
|
|
.enabled_networks
|
|
.iter()
|
|
.map(|entry| entry.key().to_string())
|
|
.collect();
|
|
app.emit("save_enabled_networks", payload)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn save_config(
|
|
&self,
|
|
app: &AppHandle,
|
|
inst_id: Uuid,
|
|
cfg: NetworkConfig,
|
|
source: PersistedConfigSource,
|
|
) -> anyhow::Result<()> {
|
|
let source = self
|
|
.network_configs
|
|
.get(&inst_id)
|
|
.map(|existing| existing.source.merge_persisted(source))
|
|
.unwrap_or(source);
|
|
let config = GUIConfig::new(inst_id.to_string(), cfg, source);
|
|
self.network_configs.insert(inst_id, config);
|
|
self.save_configs(app)
|
|
}
|
|
}
|
|
#[async_trait]
|
|
impl Storage<AppHandle, GUIConfig, anyhow::Error> for GUIStorage {
|
|
async fn insert_or_update_user_network_config(
|
|
&self,
|
|
app: AppHandle,
|
|
network_inst_id: Uuid,
|
|
network_config: NetworkConfig,
|
|
source: ConfigSource,
|
|
) -> Result<(), anyhow::Error> {
|
|
self.save_config(
|
|
&app,
|
|
network_inst_id,
|
|
network_config,
|
|
PersistedConfigSource::from_runtime_source(source),
|
|
)?;
|
|
self.enabled_networks.insert(network_inst_id);
|
|
self.save_enabled_networks(&app)?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn delete_network_configs(
|
|
&self,
|
|
app: AppHandle,
|
|
network_inst_ids: &[Uuid],
|
|
) -> Result<(), anyhow::Error> {
|
|
for network_inst_id in network_inst_ids {
|
|
self.network_configs.remove(network_inst_id);
|
|
self.enabled_networks.remove(network_inst_id);
|
|
}
|
|
self.save_configs(&app)?;
|
|
self.save_enabled_networks(&app)?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn update_network_config_state(
|
|
&self,
|
|
app: AppHandle,
|
|
network_inst_id: Uuid,
|
|
disabled: bool,
|
|
) -> Result<(), anyhow::Error> {
|
|
if disabled {
|
|
self.enabled_networks.remove(&network_inst_id);
|
|
} else {
|
|
self.enabled_networks.insert(network_inst_id);
|
|
}
|
|
self.save_enabled_networks(&app)?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn list_network_configs(
|
|
&self,
|
|
_: AppHandle,
|
|
props: ListNetworkProps,
|
|
) -> Result<Vec<GUIConfig>, anyhow::Error> {
|
|
let mut ret = Vec::new();
|
|
for entry in self.network_configs.iter() {
|
|
let id: Uuid = entry.key().to_owned();
|
|
match props {
|
|
ListNetworkProps::All => {
|
|
ret.push(entry.value().clone());
|
|
}
|
|
ListNetworkProps::EnabledOnly => {
|
|
if self.enabled_networks.contains(&id) {
|
|
ret.push(entry.value().clone());
|
|
}
|
|
}
|
|
ListNetworkProps::DisabledOnly => {
|
|
if !self.enabled_networks.contains(&id) {
|
|
ret.push(entry.value().clone());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(ret)
|
|
}
|
|
|
|
async fn get_network_config(
|
|
&self,
|
|
_: AppHandle,
|
|
network_inst_id: &str,
|
|
) -> Result<Option<GUIConfig>, anyhow::Error> {
|
|
let uuid = Uuid::parse_str(network_inst_id)?;
|
|
Ok(self
|
|
.network_configs
|
|
.get(&uuid)
|
|
.map(|entry| entry.value().clone()))
|
|
}
|
|
}
|
|
|
|
pub(super) struct GUIClientManager {
|
|
pub(super) storage: GUIStorage,
|
|
pub(super) rpc_manager: BidirectRpcManager,
|
|
}
|
|
impl GUIClientManager {
|
|
pub async fn new(
|
|
rpc_url: Option<String>,
|
|
local_process_runtime: Option<Arc<CoreProcessRuntime>>,
|
|
) -> Result<Self, anyhow::Error> {
|
|
let tunnel = if let Some(url) = rpc_url {
|
|
runtime_rpc_dialer(url.parse()?).connect().await?
|
|
} else {
|
|
local_process_runtime
|
|
.context("local RPC requires a core process runtime")?
|
|
.connect_ring_tunnel(*RPC_RING_UUID.deref())?
|
|
};
|
|
|
|
let rpc_manager = BidirectRpcManager::new();
|
|
rpc_manager.run_with_tunnel(tunnel);
|
|
|
|
Ok(Self {
|
|
storage: GUIStorage::new(),
|
|
rpc_manager,
|
|
})
|
|
}
|
|
|
|
pub fn get_enabled_instances_with_tun_ids(&self) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
|
self.storage
|
|
.network_configs
|
|
.iter()
|
|
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
|
.filter(|v| !v.config.no_tun())
|
|
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
pub fn get_enabled_instances_with_web_like_tun_ids(
|
|
&self,
|
|
) -> impl Iterator<Item = uuid::Uuid> + '_ {
|
|
self.storage
|
|
.network_configs
|
|
.iter()
|
|
.filter(|v| self.storage.enabled_networks.contains(v.key()))
|
|
.filter(|v| !v.config.no_tun())
|
|
.filter(|v| v.source.is_web_like())
|
|
.filter_map(|c| c.config.instance_id().parse::<uuid::Uuid>().ok())
|
|
}
|
|
|
|
#[cfg(target_os = "android")]
|
|
pub(super) async fn disable_instances_with_tun(
|
|
&self,
|
|
app: &AppHandle,
|
|
web_only: bool,
|
|
) -> Result<(), easytier_core::management::remote_client::RemoteClientError<anyhow::Error>>
|
|
{
|
|
let inst_ids: Vec<uuid::Uuid> = if web_only {
|
|
self.get_enabled_instances_with_web_like_tun_ids().collect()
|
|
} else {
|
|
self.get_enabled_instances_with_tun_ids().collect()
|
|
};
|
|
for inst_id in inst_ids {
|
|
self.handle_update_network_state(app.clone(), inst_id, true)
|
|
.await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) fn notify_vpn_stop_if_no_tun(&self, app: &AppHandle) -> Result<(), String> {
|
|
let has_tun = self.get_enabled_instances_with_tun_ids().any(|_| true);
|
|
if !has_tun {
|
|
app.emit("vpn_service_stop", "")
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) async fn pre_run_network_instance_hook(
|
|
&self,
|
|
app: &AppHandle,
|
|
cfg: &easytier::common::config::TomlConfigLoader,
|
|
source: PersistedConfigSource,
|
|
) -> Result<(), String> {
|
|
let instance_id = cfg.get_id();
|
|
app.emit("pre_run_network_instance", instance_id.to_string())
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
#[cfg(target_os = "android")]
|
|
if !cfg.get_flags().no_tun {
|
|
match source {
|
|
PersistedConfigSource::User | PersistedConfigSource::Legacy => {
|
|
self.disable_instances_with_tun(app, false)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
}
|
|
PersistedConfigSource::Web => {
|
|
self.disable_instances_with_tun(app, true)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
if self.get_enabled_instances_with_tun_ids().next().is_some() {
|
|
return Err(
|
|
"Android only supports one active TUN network; user-managed VPN remains active"
|
|
.to_string(),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
self.storage
|
|
.save_config(
|
|
app,
|
|
instance_id,
|
|
NetworkConfig::new_from_config(cfg).map_err(|e| e.to_string())?,
|
|
source,
|
|
)
|
|
.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) = instance_manager.instance(instance_uuid) {
|
|
if let Some(mut event_receiver) = subscribe_native_instance_event(&instance) {
|
|
let app_clone = app.clone();
|
|
let instance_id_clone = *instance_id;
|
|
tokio::spawn(async move {
|
|
let instance_id_str = instance_id_clone.to_string();
|
|
loop {
|
|
match event_receiver.recv().await {
|
|
Ok(easytier::common::global_ctx::GlobalCtxEvent::DhcpIpv4Changed(_, _)) => {
|
|
let _ = app_clone.emit("dhcp_ip_changed", &instance_id_str);
|
|
}
|
|
Ok(easytier::common::global_ctx::GlobalCtxEvent::ProxyCidrsUpdated(_, _)) => {
|
|
let _ = app_clone.emit("proxy_cidrs_updated", &instance_id_str);
|
|
}
|
|
Ok(_) => {}
|
|
Err(tokio::sync::broadcast::error::RecvError::Closed) => {
|
|
break;
|
|
}
|
|
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {
|
|
let _ = app_clone.emit("event_lagged", &instance_id_str);
|
|
event_receiver = event_receiver.resubscribe();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
self.storage.enabled_networks.insert(*instance_id);
|
|
|
|
app.emit("post_run_network_instance", instance_id.to_string())
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) async fn post_remote_remove_network_instances_hook(
|
|
&self,
|
|
app: &AppHandle,
|
|
ids: &[uuid::Uuid],
|
|
) -> Result<(), String> {
|
|
self.storage
|
|
.delete_network_configs(app.clone(), ids)
|
|
.await
|
|
.map_err(|e| e.to_string())?;
|
|
self.notify_vpn_stop_if_no_tun(app)?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) async fn post_stop_network_instances_hook(
|
|
&self,
|
|
app: &AppHandle,
|
|
) -> Result<(), String> {
|
|
self.notify_vpn_stop_if_no_tun(app)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn get_logger_rpc_client(
|
|
&self,
|
|
) -> Option<Box<dyn LoggerRpc<Controller = BaseController> + Send>> {
|
|
Some(
|
|
self.rpc_manager
|
|
.rpc_client()
|
|
.scoped_client::<LoggerRpcClientFactory<BaseController>>(1, 1, "".to_string()),
|
|
)
|
|
}
|
|
|
|
pub(super) async fn set_logging_level(&self, level: String) -> Result<(), anyhow::Error> {
|
|
let logger_rpc = self
|
|
.get_logger_rpc_client()
|
|
.ok_or_else(|| anyhow::anyhow!("Logger RPC client not available"))?;
|
|
logger_rpc
|
|
.set_logger_config(
|
|
BaseController::default(),
|
|
SetLoggerConfigRequest {
|
|
level: easytier_core::management::parse_log_level(&level).into(),
|
|
},
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|
|
|
|
pub(super) async fn load_configs(
|
|
&self,
|
|
app: AppHandle,
|
|
configs: Vec<StoredGuiConfig>,
|
|
enabled_networks: Vec<String>,
|
|
) -> anyhow::Result<()> {
|
|
self.storage.network_configs.clear();
|
|
for stored in configs {
|
|
let instance_id = stored.config.instance_id();
|
|
self.storage.network_configs.insert(
|
|
instance_id.parse()?,
|
|
GUIConfig::new(instance_id.to_string(), stored.config, stored.source),
|
|
);
|
|
}
|
|
|
|
self.storage.enabled_networks.clear();
|
|
let client = self
|
|
.get_rpc_client(app.clone())
|
|
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?;
|
|
for id in enabled_networks {
|
|
if let Ok(uuid) = id.parse()
|
|
&& !self.storage.enabled_networks.contains(&uuid)
|
|
{
|
|
let config = self
|
|
.storage
|
|
.network_configs
|
|
.get(&uuid)
|
|
.map(|i| (i.value().config.clone(), i.value().source));
|
|
let Some((config, source)) = config else {
|
|
continue;
|
|
};
|
|
let toml_config = config.gen_config()?;
|
|
self.pre_run_network_instance_hook(&app, &toml_config, source)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!(e))?;
|
|
client
|
|
.run_network_instance(
|
|
BaseController::default(),
|
|
RunNetworkInstanceRequest {
|
|
inst_id: None,
|
|
config: Some(config),
|
|
overwrite: false,
|
|
source: config_source_to_rpc(source.to_runtime_source()),
|
|
},
|
|
)
|
|
.await?;
|
|
self.post_run_network_instance_hook(&app, &uuid)
|
|
.await
|
|
.map_err(|e| anyhow::anyhow!(e))?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
impl RemoteClientManager<AppHandle, GUIConfig, anyhow::Error> for GUIClientManager {
|
|
fn get_rpc_client(
|
|
&self,
|
|
_: AppHandle,
|
|
) -> Option<Box<dyn WebClientService<Controller = BaseController> + Send>> {
|
|
Some(
|
|
self.rpc_manager
|
|
.rpc_client()
|
|
.scoped_client::<WebClientServiceClientFactory<BaseController>>(
|
|
1,
|
|
1,
|
|
"".to_string(),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn get_storage(&self) -> &impl Storage<AppHandle, GUIConfig, anyhow::Error> {
|
|
&self.storage
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{PersistedConfigSource, StoredGuiConfig};
|
|
use easytier::proto::api::manage::NetworkConfig;
|
|
|
|
#[test]
|
|
fn stored_gui_config_defaults_missing_source_to_legacy() {
|
|
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
|
|
"config": NetworkConfig::default(),
|
|
}))
|
|
.unwrap();
|
|
assert_eq!(stored.source, PersistedConfigSource::Legacy);
|
|
}
|
|
|
|
#[test]
|
|
fn stored_gui_config_deserializes_webhook_source_as_web() {
|
|
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
|
|
"config": NetworkConfig::default(),
|
|
"source": "webhook",
|
|
}))
|
|
.unwrap();
|
|
assert_eq!(stored.source, PersistedConfigSource::Web);
|
|
}
|
|
|
|
#[test]
|
|
fn stored_gui_config_defaults_unknown_source_to_legacy() {
|
|
let stored: StoredGuiConfig = serde_json::from_value(serde_json::json!({
|
|
"config": NetworkConfig::default(),
|
|
"source": "unknown",
|
|
}))
|
|
.unwrap();
|
|
assert_eq!(stored.source, PersistedConfigSource::Legacy);
|
|
}
|
|
|
|
#[test]
|
|
fn persisted_source_merge_keeps_legacy_and_web_over_ambiguous_user() {
|
|
assert_eq!(
|
|
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::User),
|
|
PersistedConfigSource::Legacy
|
|
);
|
|
assert_eq!(
|
|
PersistedConfigSource::Web.merge_persisted(PersistedConfigSource::User),
|
|
PersistedConfigSource::Web
|
|
);
|
|
assert_eq!(
|
|
PersistedConfigSource::Legacy.merge_persisted(PersistedConfigSource::Web),
|
|
PersistedConfigSource::Web
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn only_web_configs_are_web_like() {
|
|
assert!(!PersistedConfigSource::Legacy.is_web_like());
|
|
assert!(!PersistedConfigSource::User.is_web_like());
|
|
assert!(PersistedConfigSource::Web.is_web_like());
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(not(target_os = "android"))]
|
|
mod service {
|
|
use anyhow::Context;
|
|
|
|
#[derive(Clone, serde::Serialize, serde::Deserialize)]
|
|
pub struct ServiceOptions {
|
|
pub(super) config_dir: String,
|
|
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> {
|
|
let mut args = vec![
|
|
"--config-dir".into(),
|
|
self.config_dir.clone().into(),
|
|
"--rpc-portal".into(),
|
|
self.rpc_portal.clone().into(),
|
|
"--file-log-level".into(),
|
|
self.file_log_level.clone().into(),
|
|
"--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
|
|
}
|
|
}
|
|
|
|
pub fn install(opts: ServiceOptions) -> anyhow::Result<()> {
|
|
let service = easytier::service_manager::Service::new(env!("CARGO_PKG_NAME").to_string())?;
|
|
let options = easytier::service_manager::ServiceInstallOptions {
|
|
program: super::get_exe_path().into(),
|
|
args: opts.to_args_vec(),
|
|
work_directory: std::env::current_dir()?,
|
|
disable_autostart: false,
|
|
description: Some("EasyTier Gui Service".to_string()),
|
|
display_name: Some("EasyTier Gui Service".to_string()),
|
|
disable_restart_on_failure: false,
|
|
};
|
|
service
|
|
.install(&options)
|
|
.with_context(|| "Failed to install service")?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn uninstall() -> anyhow::Result<()> {
|
|
let service = easytier::service_manager::Service::new(env!("CARGO_PKG_NAME").to_string())?;
|
|
service.uninstall()?;
|
|
Ok(())
|
|
}
|
|
|
|
pub fn set_status(enable: bool) -> anyhow::Result<()> {
|
|
use easytier::service_manager::*;
|
|
let service = Service::new(env!("CARGO_PKG_NAME").to_string())?;
|
|
let status = service.status()?;
|
|
if enable && status != ServiceStatus::Running {
|
|
service.start().with_context(|| "Failed to start service")?;
|
|
} else if !enable && status == ServiceStatus::Running {
|
|
service.stop().with_context(|| "Failed to stop service")?;
|
|
} else if status == ServiceStatus::NotInstalled {
|
|
return Err(anyhow::anyhow!("Service not installed"));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn status() -> anyhow::Result<easytier::service_manager::ServiceStatus> {
|
|
let service = easytier::service_manager::Service::new(env!("CARGO_PKG_NAME").to_string())?;
|
|
service.status()
|
|
}
|
|
}
|
|
|
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
|
pub fn run_gui() -> std::process::ExitCode {
|
|
#[cfg(not(target_os = "android"))]
|
|
if !check_sudo() {
|
|
use std::process;
|
|
process::exit(0);
|
|
}
|
|
|
|
setup_panic_handler();
|
|
|
|
let mut builder = tauri::Builder::default();
|
|
|
|
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
|
{
|
|
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
|
app.webview_windows()
|
|
.values()
|
|
.next()
|
|
.expect("Sorry, no window found")
|
|
.set_focus()
|
|
.expect("Can't Bring Window to Focus");
|
|
}));
|
|
}
|
|
|
|
builder = builder
|
|
.plugin(tauri_plugin_os::init())
|
|
.plugin(tauri_plugin_clipboard_manager::init())
|
|
.plugin(tauri_plugin_process::init())
|
|
.plugin(tauri_plugin_shell::init())
|
|
.plugin(tauri_plugin_vpnservice::init());
|
|
|
|
let app = builder
|
|
.setup(|app| {
|
|
// for logging config
|
|
let Ok(log_dir) = get_log_dir(app.app_handle()) else {
|
|
return Ok(());
|
|
};
|
|
let config = LoggingConfigBuilder::default()
|
|
.file_logger(FileLoggerConfig {
|
|
dir: Some(log_dir.to_string_lossy().to_string()),
|
|
level: None,
|
|
file: None,
|
|
size_mb: None,
|
|
count: None,
|
|
})
|
|
.build()
|
|
.map_err(|e| e.to_string())?;
|
|
let Ok(_) = log::init(&config, true) else {
|
|
return Ok(());
|
|
};
|
|
|
|
// for tray icon, menu need to be built in js
|
|
#[cfg(not(target_os = "android"))]
|
|
let _tray_menu = TrayIconBuilder::with_id("main")
|
|
.show_menu_on_left_click(false)
|
|
.on_tray_icon_event(|tray, event| {
|
|
if let TrayIconEvent::Click {
|
|
button: MouseButton::Left,
|
|
button_state: MouseButtonState::Up,
|
|
..
|
|
} = event
|
|
{
|
|
let app = tray.app_handle();
|
|
toggle_window_visibility(app);
|
|
}
|
|
})
|
|
.icon(tauri::image::Image::from_bytes(include_bytes!(
|
|
"../icons/icon.png"
|
|
))?)
|
|
.icon_as_template(true)
|
|
.build(app)?;
|
|
|
|
Ok(())
|
|
})
|
|
.invoke_handler(tauri::generate_handler![
|
|
parse_network_config,
|
|
generate_network_config,
|
|
run_network_instance,
|
|
collect_network_info,
|
|
set_logging_level,
|
|
set_tun_fd,
|
|
easytier_version,
|
|
set_dock_visibility,
|
|
list_network_instance_ids,
|
|
remove_network_instance,
|
|
update_network_config_state,
|
|
save_network_config,
|
|
validate_config,
|
|
get_config,
|
|
load_configs,
|
|
get_network_metas,
|
|
init_service,
|
|
set_service_status,
|
|
get_service_status,
|
|
init_rpc_connection,
|
|
is_client_running,
|
|
init_web_client,
|
|
is_web_client_connected,
|
|
get_log_dir_path,
|
|
])
|
|
.on_window_event(|_win, event| match event {
|
|
#[cfg(not(target_os = "android"))]
|
|
tauri::WindowEvent::CloseRequested { api, .. } => {
|
|
let _ = _win.hide();
|
|
let _ = set_dock_visibility(_win.app_handle().clone(), false);
|
|
api.prevent_close();
|
|
}
|
|
_ => {}
|
|
})
|
|
.build(tauri::generate_context!())
|
|
.unwrap();
|
|
|
|
app.run(|_app, _event| {});
|
|
|
|
std::process::ExitCode::SUCCESS
|
|
}
|
|
|
|
pub fn run_cli() -> std::process::ExitCode {
|
|
tokio::runtime::Builder::new_current_thread()
|
|
.enable_all()
|
|
.build()
|
|
.unwrap()
|
|
.block_on(async { easytier::core::main().await })
|
|
}
|