refactor(gui): refactor gui to use RemoteClient trait and RemoteManagement component (#1489)

* refactor(gui): refactor gui to use RemoteClient trait and RemoteManagement component
* feat(gui): Add network config saving and refactor RemoteManagement
This commit is contained in:
Mg Pig
2025-10-20 22:07:01 +08:00
committed by GitHub
parent 67ac9b00ff
commit eba9504fc2
27 changed files with 1040 additions and 793 deletions

View File

@@ -52,6 +52,7 @@ tauri-plugin-vpnservice = { path = "../../tauri-plugin-vpnservice" }
tauri-plugin-os = "2.3.0"
tauri-plugin-autostart = "2.5.0"
uuid = "1.17.0"
async-trait = "0.1.89"
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.52", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] }

View File

@@ -3,28 +3,44 @@
mod elevate;
use std::collections::BTreeMap;
use easytier::proto::api::manage::{
CollectNetworkInfoResponse, ValidateConfigResponse, WebClientService,
WebClientServiceClientFactory,
};
use easytier::rpc_service::remote_client::{
ListNetworkInstanceIdsJsonResp, ListNetworkProps, RemoteClientManager, Storage,
};
use easytier::{
common::config::{ConfigLoader, FileLoggerConfig, LoggingConfigBuilder, TomlConfigLoader},
instance_manager::NetworkInstanceManager,
launcher::{ConfigSource, NetworkConfig, NetworkInstanceRunningInfo},
launcher::NetworkConfig,
rpc_service::ApiRpcServer,
tunnel::ring::RingTunnelListener,
utils::{self, NewFilterSender},
};
use std::ops::Deref;
use std::sync::Arc;
use uuid::Uuid;
use tauri::Manager as _;
pub const AUTOSTART_ARG: &str = "--autostart";
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<NetworkInstanceManager> =
once_cell::sync::Lazy::new(NetworkInstanceManager::new);
pub const AUTOSTART_ARG: &str = "--autostart";
static INSTANCE_MANAGER: once_cell::sync::Lazy<Arc<NetworkInstanceManager>> =
once_cell::sync::Lazy::new(|| Arc::new(NetworkInstanceManager::new()));
static mut LOGGER_LEVEL_SENDER: once_cell::sync::Lazy<Option<NewFilterSender>> =
once_cell::sync::Lazy::new(Default::default);
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::OnceCell<manager::GUIClientManager> =
once_cell::sync::OnceCell::new();
#[tauri::command]
fn easytier_version() -> Result<String, String> {
Ok(easytier::VERSION.to_string())
@@ -47,14 +63,6 @@ fn set_dock_visibility(app: tauri::AppHandle, visible: bool) -> Result<(), Strin
Ok(())
}
#[tauri::command]
fn is_autostart() -> Result<bool, String> {
let args: Vec<String> = std::env::args().collect();
println!("{:?}", args);
Ok(args.contains(&AUTOSTART_ARG.to_owned()))
}
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn parse_network_config(cfg: NetworkConfig) -> Result<String, String> {
let toml = cfg.gen_config().map_err(|e| e.to_string())?;
@@ -69,47 +77,48 @@ fn generate_network_config(toml_config: String) -> Result<NetworkConfig, String>
}
#[tauri::command]
fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> {
async fn run_network_instance(app: AppHandle, cfg: NetworkConfig) -> Result<(), String> {
let instance_id = cfg.instance_id().to_string();
let cfg = cfg.gen_config().map_err(|e| e.to_string())?;
INSTANCE_MANAGER
.run_network_instance(cfg, ConfigSource::GUI)
.map_err(|e| e.to_string())?;
println!("instance {} started", instance_id);
Ok(())
}
#[tauri::command]
fn retain_network_instance(instance_ids: Vec<String>) -> Result<(), String> {
let instance_ids = instance_ids
.into_iter()
.filter_map(|id| uuid::Uuid::parse_str(&id).ok())
.collect();
let retained = INSTANCE_MANAGER
.retain_network_instance(instance_ids)
app.emit("pre_run_network_instance", cfg.instance_id())
.map_err(|e| e.to_string())?;
println!("instance {:?} retained", retained);
Ok(())
}
#[tauri::command]
async fn collect_network_infos() -> Result<BTreeMap<String, NetworkInstanceRunningInfo>, String> {
let infos = INSTANCE_MANAGER
.collect_network_infos()
#[cfg(target_os = "android")]
if cfg.no_tun() == false {
CLIENT_MANAGER
.get()
.unwrap()
.disable_instances_with_tun(&app)
.await
.map_err(|e| e.to_string())?;
}
CLIENT_MANAGER
.get()
.unwrap()
.handle_run_network_instance(app.clone(), cfg)
.await
.map_err(|e| e.to_string())?;
let mut ret = BTreeMap::new();
for (uuid, info) in infos {
ret.insert(uuid.to_string(), info);
}
Ok(ret)
app.emit("post_run_network_instance", instance_id)
.map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
fn get_os_hostname() -> Result<String, String> {
Ok(gethostname::gethostname().to_string_lossy().to_string())
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())?;
CLIENT_MANAGER
.get()
.unwrap()
.handle_collect_network_info(app, Some(vec![instance_id]))
.await
.map_err(|e| e.to_string())
}
#[tauri::command]
@@ -121,10 +130,121 @@ fn set_logging_level(level: String) -> Result<(), String> {
}
#[tauri::command]
fn set_tun_fd(instance_id: String, fd: i32) -> Result<(), String> {
let uuid = uuid::Uuid::parse_str(&instance_id).map_err(|e| e.to_string())?;
INSTANCE_MANAGER
.set_tun_fd(&uuid, fd)
fn set_tun_fd(fd: i32) -> Result<(), String> {
if let Some(uuid) = CLIENT_MANAGER
.get()
.unwrap()
.get_enabled_instances_with_tun_ids()
.next()
{
INSTANCE_MANAGER
.set_tun_fd(&uuid, fd)
.map_err(|e| e.to_string())?;
}
Ok(())
}
#[tauri::command]
async fn list_network_instance_ids(
app: AppHandle,
) -> Result<ListNetworkInstanceIdsJsonResp, String> {
CLIENT_MANAGER
.get()
.unwrap()
.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())?;
CLIENT_MANAGER
.get()
.unwrap()
.handle_remove_network_instances(app.clone(), vec![instance_id])
.await
.map_err(|e| e.to_string())?;
CLIENT_MANAGER
.get()
.unwrap()
.notify_vpn_stop_if_no_tun(&app)?;
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())?;
CLIENT_MANAGER
.get()
.unwrap()
.handle_update_network_state(app.clone(), instance_id, disabled)
.await
.map_err(|e| e.to_string())?;
if disabled {
CLIENT_MANAGER
.get()
.unwrap()
.notify_vpn_stop_if_no_tun(&app)?;
}
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())?;
CLIENT_MANAGER
.get()
.unwrap()
.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> {
CLIENT_MANAGER
.get()
.unwrap()
.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 cfg = CLIENT_MANAGER
.get()
.unwrap()
.storage
.get_network_config(app, &instance_id)
.await
.map_err(|e| e.to_string())?
.ok_or_else(|| format!("Config not found for instance ID: {}", instance_id))?;
Ok(cfg.1)
}
#[tauri::command]
fn load_configs(configs: Vec<NetworkConfig>, enabled_networks: Vec<String>) -> Result<(), String> {
CLIENT_MANAGER
.get()
.unwrap()
.storage
.load_configs(configs, enabled_networks)
.map_err(|e| e.to_string())?;
Ok(())
}
@@ -166,6 +286,266 @@ fn check_sudo() -> bool {
is_elevated
}
mod manager {
use super::*;
use async_trait::async_trait;
use dashmap::{DashMap, DashSet};
use easytier::launcher::{ConfigSource, NetworkConfig};
use easytier::proto::rpc_impl::bidirect::BidirectRpcManager;
use easytier::proto::rpc_types::controller::BaseController;
use easytier::rpc_service::remote_client::PersistentConfig;
use easytier::tunnel::ring::RingTunnelConnector;
use easytier::tunnel::TunnelConnector;
#[derive(Clone)]
pub(super) struct GUIConfig(String, pub(crate) NetworkConfig);
impl PersistentConfig<anyhow::Error> for GUIConfig {
fn get_network_inst_id(&self) -> &str {
&self.0
}
fn get_network_config(&self) -> Result<NetworkConfig, anyhow::Error> {
Ok(self.1.clone())
}
}
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(),
}
}
pub(super) fn load_configs(
&self,
configs: Vec<NetworkConfig>,
enabled_networks: Vec<String>,
) -> anyhow::Result<()> {
self.network_configs.clear();
for cfg in configs {
let instance_id = cfg.instance_id();
self.network_configs.insert(
instance_id.parse()?,
GUIConfig(instance_id.to_string(), cfg),
);
}
self.enabled_networks.clear();
INSTANCE_MANAGER
.filter_network_instance(|_, _| true)
.into_iter()
.for_each(|id| {
self.enabled_networks.insert(id);
});
for id in enabled_networks {
if let Ok(uuid) = id.parse() {
if !self.enabled_networks.contains(&uuid) {
let config = self
.network_configs
.get(&uuid)
.map(|i| i.value().1.gen_config())
.ok_or_else(|| anyhow::anyhow!("Config not found"))??;
INSTANCE_MANAGER.run_network_instance(config, ConfigSource::GUI)?;
self.enabled_networks.insert(uuid);
}
}
}
Ok(())
}
fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> {
let configs: Result<Vec<String>, _> = self
.network_configs
.iter()
.map(|entry| serde_json::to_string(&entry.value().1))
.collect();
let payload = format!("[{}]", configs?.join(","));
app.emit_str("save_configs", payload)?;
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,
) -> anyhow::Result<()> {
let config = GUIConfig(inst_id.to_string(), cfg);
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,
) -> Result<(), anyhow::Error> {
self.save_config(&app, network_inst_id, network_config)?;
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)
}
async fn update_network_config_state(
&self,
app: AppHandle,
network_inst_id: Uuid,
disabled: bool,
) -> Result<GUIConfig, anyhow::Error> {
if disabled {
self.enabled_networks.remove(&network_inst_id);
} else {
self.enabled_networks.insert(network_inst_id);
}
self.save_enabled_networks(&app)?;
let cfg = self
.network_configs
.get(&network_inst_id)
.ok_or_else(|| anyhow::anyhow!("Config not found"))?;
Ok(cfg.value().clone())
}
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,
rpc_manager: BidirectRpcManager,
}
impl GUIClientManager {
pub async fn new() -> Result<Self, anyhow::Error> {
let mut connector = RingTunnelConnector::new(
format!("ring://{}", RPC_RING_UUID.deref()).parse().unwrap(),
);
let tunnel = connector.connect().await?;
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.1.no_tun())
.filter_map(|c| c.1.instance_id().parse::<uuid::Uuid>().ok())
}
#[cfg(target_os = "android")]
pub(super) async fn disable_instances_with_tun(
&self,
app: &AppHandle,
) -> Result<(), easytier::rpc_service::remote_client::RemoteClientError<anyhow::Error>>
{
for inst_id in self.get_enabled_instances_with_tun_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(())
}
}
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_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
#[cfg(not(target_os = "android"))]
@@ -176,6 +556,24 @@ pub fn run() {
utils::setup_panic_handler();
let _rpc_server_handle = tauri::async_runtime::spawn(async move {
let rpc_server = ApiRpcServer::from_tunnel(
RingTunnelListener::new(format!("ring://{}", RPC_RING_UUID.deref()).parse().unwrap()),
INSTANCE_MANAGER.clone(),
)
.serve()
.await
.expect("Failed to start RPC server");
let _ = CLIENT_MANAGER.set(
manager::GUIClientManager::new()
.await
.expect("Failed to create GUI client manager"),
);
rpc_server
});
let mut builder = tauri::Builder::default();
#[cfg(not(target_os = "android"))]
@@ -257,14 +655,18 @@ pub fn run() {
parse_network_config,
generate_network_config,
run_network_instance,
retain_network_instance,
collect_network_infos,
get_os_hostname,
collect_network_info,
set_logging_level,
set_tun_fd,
is_autostart,
easytier_version,
set_dock_visibility
set_dock_visibility,
list_network_instance_ids,
remove_network_instance,
update_network_config_state,
save_network_config,
validate_config,
get_config,
load_configs,
])
.on_window_event(|_win, event| match event {
#[cfg(not(target_os = "android"))]

View File

@@ -10,7 +10,7 @@ declare global {
const MenuItemExit: typeof import('./composables/tray')['MenuItemExit']
const MenuItemShow: typeof import('./composables/tray')['MenuItemShow']
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
const collectNetworkInfos: typeof import('./composables/network')['collectNetworkInfos']
const collectNetworkInfo: typeof import('./composables/backend')['collectNetworkInfo']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const createPinia: typeof import('pinia')['createPinia']
@@ -18,22 +18,24 @@ declare global {
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const defineStore: typeof import('pinia')['defineStore']
const deleteNetworkInstance: typeof import('./composables/backend')['deleteNetworkInstance']
const effectScope: typeof import('vue')['effectScope']
const generateMenuItem: typeof import('./composables/tray')['generateMenuItem']
const generateNetworkConfig: typeof import('./composables/network')['generateNetworkConfig']
const generateNetworkConfig: typeof import('./composables/backend')['generateNetworkConfig']
const getActivePinia: typeof import('pinia')['getActivePinia']
const getConfig: typeof import('./composables/backend')['getConfig']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getEasytierVersion: typeof import('./composables/network')['getEasytierVersion']
const getOsHostname: typeof import('./composables/network')['getOsHostname']
const getEasytierVersion: typeof import('./composables/backend')['getEasytierVersion']
const h: typeof import('vue')['h']
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
const inject: typeof import('vue')['inject']
const isAutostart: typeof import('./composables/network')['isAutostart']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const listNetworkInstanceIds: typeof import('./composables/backend')['listNetworkInstanceIds']
const listenGlobalEvents: typeof import('./composables/event')['listenGlobalEvents']
const mapActions: typeof import('pinia')['mapActions']
const mapGetters: typeof import('pinia')['mapGetters']
const mapState: typeof import('pinia')['mapState']
@@ -50,6 +52,7 @@ declare global {
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onNetworkInstanceChange: typeof import('./composables/mobile_vpn')['onNetworkInstanceChange']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
@@ -57,22 +60,23 @@ declare global {
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
const parseNetworkConfig: typeof import('./composables/network')['parseNetworkConfig']
const parseNetworkConfig: typeof import('./composables/backend')['parseNetworkConfig']
const prepareVpnService: typeof import('./composables/mobile_vpn')['prepareVpnService']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const retainNetworkInstance: typeof import('./composables/network')['retainNetworkInstance']
const runNetworkInstance: typeof import('./composables/network')['runNetworkInstance']
const runNetworkInstance: typeof import('./composables/backend')['runNetworkInstance']
const saveNetworkConfig: typeof import('./composables/backend')['saveNetworkConfig']
const sendConfigs: typeof import('./composables/backend')['sendConfigs']
const setActivePinia: typeof import('pinia')['setActivePinia']
const setLoggingLevel: typeof import('./composables/network')['setLoggingLevel']
const setLoggingLevel: typeof import('./composables/backend')['setLoggingLevel']
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
const setTrayMenu: typeof import('./composables/tray')['setTrayMenu']
const setTrayRunState: typeof import('./composables/tray')['setTrayRunState']
const setTrayTooltip: typeof import('./composables/tray')['setTrayTooltip']
const setTunFd: typeof import('./composables/network')['setTunFd']
const setTunFd: typeof import('./composables/backend')['setTunFd']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
@@ -83,6 +87,7 @@ declare global {
const toValue: typeof import('vue')['toValue']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const updateNetworkConfigState: typeof import('./composables/backend')['updateNetworkConfigState']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
@@ -90,12 +95,12 @@ declare global {
const useId: typeof import('vue')['useId']
const useLink: typeof import('vue-router/auto')['useLink']
const useModel: typeof import('vue')['useModel']
const useNetworkStore: typeof import('./stores/network')['useNetworkStore']
const useRoute: typeof import('vue-router')['useRoute']
const useRouter: typeof import('vue-router')['useRouter']
const useSlots: typeof import('vue')['useSlots']
const useTemplateRef: typeof import('vue')['useTemplateRef']
const useTray: typeof import('./composables/tray')['useTray']
const validateConfig: typeof import('./composables/backend')['validateConfig']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
@@ -117,7 +122,7 @@ declare module 'vue' {
readonly MenuItemExit: UnwrapRef<typeof import('./composables/tray')['MenuItemExit']>
readonly MenuItemShow: UnwrapRef<typeof import('./composables/tray')['MenuItemShow']>
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
readonly collectNetworkInfos: UnwrapRef<typeof import('./composables/network')['collectNetworkInfos']>
readonly collectNetworkInfo: UnwrapRef<typeof import('./composables/backend')['collectNetworkInfo']>
readonly computed: UnwrapRef<typeof import('vue')['computed']>
readonly createApp: UnwrapRef<typeof import('vue')['createApp']>
readonly createPinia: UnwrapRef<typeof import('pinia')['createPinia']>
@@ -125,22 +130,24 @@ declare module 'vue' {
readonly defineAsyncComponent: UnwrapRef<typeof import('vue')['defineAsyncComponent']>
readonly defineComponent: UnwrapRef<typeof import('vue')['defineComponent']>
readonly defineStore: UnwrapRef<typeof import('pinia')['defineStore']>
readonly deleteNetworkInstance: UnwrapRef<typeof import('./composables/backend')['deleteNetworkInstance']>
readonly effectScope: UnwrapRef<typeof import('vue')['effectScope']>
readonly generateMenuItem: UnwrapRef<typeof import('./composables/tray')['generateMenuItem']>
readonly generateNetworkConfig: UnwrapRef<typeof import('./composables/network')['generateNetworkConfig']>
readonly generateNetworkConfig: UnwrapRef<typeof import('./composables/backend')['generateNetworkConfig']>
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
readonly getConfig: UnwrapRef<typeof import('./composables/backend')['getConfig']>
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/network')['getEasytierVersion']>
readonly getOsHostname: UnwrapRef<typeof import('./composables/network')['getOsHostname']>
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/backend')['getEasytierVersion']>
readonly h: UnwrapRef<typeof import('vue')['h']>
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
readonly inject: UnwrapRef<typeof import('vue')['inject']>
readonly isAutostart: UnwrapRef<typeof import('./composables/network')['isAutostart']>
readonly isProxy: UnwrapRef<typeof import('vue')['isProxy']>
readonly isReactive: UnwrapRef<typeof import('vue')['isReactive']>
readonly isReadonly: UnwrapRef<typeof import('vue')['isReadonly']>
readonly isRef: UnwrapRef<typeof import('vue')['isRef']>
readonly listNetworkInstanceIds: UnwrapRef<typeof import('./composables/backend')['listNetworkInstanceIds']>
readonly listenGlobalEvents: UnwrapRef<typeof import('./composables/event')['listenGlobalEvents']>
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
@@ -157,6 +164,7 @@ declare module 'vue' {
readonly onDeactivated: UnwrapRef<typeof import('vue')['onDeactivated']>
readonly onErrorCaptured: UnwrapRef<typeof import('vue')['onErrorCaptured']>
readonly onMounted: UnwrapRef<typeof import('vue')['onMounted']>
readonly onNetworkInstanceChange: UnwrapRef<typeof import('./composables/mobile_vpn')['onNetworkInstanceChange']>
readonly onRenderTracked: UnwrapRef<typeof import('vue')['onRenderTracked']>
readonly onRenderTriggered: UnwrapRef<typeof import('vue')['onRenderTriggered']>
readonly onScopeDispose: UnwrapRef<typeof import('vue')['onScopeDispose']>
@@ -164,22 +172,23 @@ declare module 'vue' {
readonly onUnmounted: UnwrapRef<typeof import('vue')['onUnmounted']>
readonly onUpdated: UnwrapRef<typeof import('vue')['onUpdated']>
readonly onWatcherCleanup: UnwrapRef<typeof import('vue')['onWatcherCleanup']>
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/network')['parseNetworkConfig']>
readonly parseNetworkConfig: UnwrapRef<typeof import('./composables/backend')['parseNetworkConfig']>
readonly prepareVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['prepareVpnService']>
readonly provide: UnwrapRef<typeof import('vue')['provide']>
readonly reactive: UnwrapRef<typeof import('vue')['reactive']>
readonly readonly: UnwrapRef<typeof import('vue')['readonly']>
readonly ref: UnwrapRef<typeof import('vue')['ref']>
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
readonly retainNetworkInstance: UnwrapRef<typeof import('./composables/network')['retainNetworkInstance']>
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/network')['runNetworkInstance']>
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/backend')['runNetworkInstance']>
readonly saveNetworkConfig: UnwrapRef<typeof import('./composables/backend')['saveNetworkConfig']>
readonly sendConfigs: UnwrapRef<typeof import('./composables/backend')['sendConfigs']>
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/network')['setLoggingLevel']>
readonly setLoggingLevel: UnwrapRef<typeof import('./composables/backend')['setLoggingLevel']>
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
readonly setTrayRunState: UnwrapRef<typeof import('./composables/tray')['setTrayRunState']>
readonly setTrayTooltip: UnwrapRef<typeof import('./composables/tray')['setTrayTooltip']>
readonly setTunFd: UnwrapRef<typeof import('./composables/network')['setTunFd']>
readonly setTunFd: UnwrapRef<typeof import('./composables/backend')['setTunFd']>
readonly shallowReactive: UnwrapRef<typeof import('vue')['shallowReactive']>
readonly shallowReadonly: UnwrapRef<typeof import('vue')['shallowReadonly']>
readonly shallowRef: UnwrapRef<typeof import('vue')['shallowRef']>
@@ -190,6 +199,7 @@ declare module 'vue' {
readonly toValue: UnwrapRef<typeof import('vue')['toValue']>
readonly triggerRef: UnwrapRef<typeof import('vue')['triggerRef']>
readonly unref: UnwrapRef<typeof import('vue')['unref']>
readonly updateNetworkConfigState: UnwrapRef<typeof import('./composables/backend')['updateNetworkConfigState']>
readonly useAttrs: UnwrapRef<typeof import('vue')['useAttrs']>
readonly useCssModule: UnwrapRef<typeof import('vue')['useCssModule']>
readonly useCssVars: UnwrapRef<typeof import('vue')['useCssVars']>
@@ -197,12 +207,12 @@ declare module 'vue' {
readonly useId: UnwrapRef<typeof import('vue')['useId']>
readonly useLink: UnwrapRef<typeof import('vue-router/auto')['useLink']>
readonly useModel: UnwrapRef<typeof import('vue')['useModel']>
readonly useNetworkStore: UnwrapRef<typeof import('./stores/network')['useNetworkStore']>
readonly useRoute: UnwrapRef<typeof import('vue-router')['useRoute']>
readonly useRouter: UnwrapRef<typeof import('vue-router')['useRouter']>
readonly useSlots: UnwrapRef<typeof import('vue')['useSlots']>
readonly useTemplateRef: UnwrapRef<typeof import('vue')['useTemplateRef']>
readonly useTray: UnwrapRef<typeof import('./composables/tray')['useTray']>
readonly validateConfig: UnwrapRef<typeof import('./composables/backend')['validateConfig']>
readonly watch: UnwrapRef<typeof import('vue')['watch']>
readonly watchEffect: UnwrapRef<typeof import('vue')['watchEffect']>
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts">
import { getEasytierVersion } from '~/composables/network'
import { getEasytierVersion } from '~/composables/backend'
const { t } = useI18n()

View File

@@ -0,0 +1,65 @@
import { invoke } from '@tauri-apps/api/core'
import { Api, type NetworkTypes } from 'easytier-frontend-lib'
import { getAutoLaunchStatusAsync } from '~/modules/auto_launch'
type NetworkConfig = NetworkTypes.NetworkConfig
type ValidateConfigResponse = Api.ValidateConfigResponse
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
export async function parseNetworkConfig(cfg: NetworkConfig) {
return invoke<string>('parse_network_config', { cfg })
}
export async function generateNetworkConfig(tomlConfig: string) {
return invoke<NetworkConfig>('generate_network_config', { tomlConfig })
}
export async function runNetworkInstance(cfg: NetworkConfig) {
return invoke('run_network_instance', { cfg })
}
export async function collectNetworkInfo(instanceId: string) {
return await invoke<Api.CollectNetworkInfoResponse>('collect_network_info', { instanceId })
}
export async function setLoggingLevel(level: string) {
return await invoke('set_logging_level', { level })
}
export async function setTunFd(fd: number) {
return await invoke('set_tun_fd', { fd })
}
export async function getEasytierVersion() {
return await invoke<string>('easytier_version')
}
export async function listNetworkInstanceIds() {
return await invoke<ListNetworkInstanceIdResponse>('list_network_instance_ids')
}
export async function deleteNetworkInstance(instanceId: string) {
return await invoke('remove_network_instance', { instanceId })
}
export async function updateNetworkConfigState(instanceId: string, disabled: boolean) {
return await invoke('update_network_config_state', { instanceId, disabled })
}
export async function saveNetworkConfig(cfg: NetworkConfig) {
return await invoke('save_network_config', { cfg })
}
export async function validateConfig(cfg: NetworkConfig) {
return await invoke<ValidateConfigResponse>('validate_config', { cfg })
}
export async function getConfig(instanceId: string) {
return await invoke<NetworkConfig>('get_config', { instanceId })
}
export async function sendConfigs() {
let networkList: NetworkConfig[] = JSON.parse(localStorage.getItem('networkList') || '[]');
let autoStartInstIds = getAutoLaunchStatusAsync() ? JSON.parse(localStorage.getItem('autoStartInstIds') || '[]') : []
return await invoke('load_configs', { configs: networkList, enabledNetworks: autoStartInstIds })
}

View File

@@ -0,0 +1,51 @@
import { Event, listen } from "@tauri-apps/api/event";
import { type } from "@tauri-apps/plugin-os";
import { NetworkTypes } from "easytier-frontend-lib"
const EVENTS = Object.freeze({
SAVE_CONFIGS: 'save_configs',
SAVE_ENABLED_NETWORKS: 'save_enabled_networks',
PRE_RUN_NETWORK_INSTANCE: 'pre_run_network_instance',
POST_RUN_NETWORK_INSTANCE: 'post_run_network_instance',
VPN_SERVICE_STOP: 'vpn_service_stop',
});
function onSaveConfigs(event: Event<NetworkTypes.NetworkConfig[]>) {
console.log(`Received event '${EVENTS.SAVE_CONFIGS}': ${event.payload}`);
localStorage.setItem('networkList', JSON.stringify(event.payload));
}
function onSaveEnabledNetworks(event: Event<string[]>) {
console.log(`Received event '${EVENTS.SAVE_ENABLED_NETWORKS}': ${event.payload}`);
localStorage.setItem('autoStartInstIds', JSON.stringify(event.payload));
}
async function onPreRunNetworkInstance(event: Event<string>) {
if (type() === 'android') {
await prepareVpnService(event.payload);
}
}
async function onPostRunNetworkInstance(event: Event<string>) {
if (type() === 'android') {
await onNetworkInstanceChange(event.payload);
}
}
async function onVpnServiceStop(event: Event<string>) {
await onNetworkInstanceChange(event.payload);
}
export async function listenGlobalEvents() {
const unlisteners = [
await listen(EVENTS.SAVE_CONFIGS, onSaveConfigs),
await listen(EVENTS.SAVE_ENABLED_NETWORKS, onSaveEnabledNetworks),
await listen(EVENTS.PRE_RUN_NETWORK_INSTANCE, onPreRunNetworkInstance),
await listen(EVENTS.POST_RUN_NETWORK_INSTANCE, onPostRunNetworkInstance),
await listen(EVENTS.VPN_SERVICE_STOP, onVpnServiceStop),
];
return () => {
unlisteners.forEach(unlisten => unlisten());
};
}

View File

@@ -5,8 +5,6 @@ import { prepare_vpn, start_vpn, stop_vpn } from 'tauri-plugin-vpnservice-api'
type Route = NetworkTypes.Route
const networkStore = useNetworkStore()
interface vpnStatus {
running: boolean
ipv4Addr: string | null | undefined
@@ -69,7 +67,7 @@ async function onVpnServiceStart(payload: any) {
console.log('vpn service start', JSON.stringify(payload))
curVpnStatus.running = true
if (payload.fd) {
setTunFd(networkStore.networkInstanceIds[0], payload.fd)
setTunFd(payload.fd)
}
}
@@ -116,20 +114,17 @@ function getRoutesForVpn(routes: Route[], node_config: NetworkTypes.NetworkConfi
return Array.from(new Set(ret)).sort()
}
async function onNetworkInstanceChange() {
console.error('vpn service watch network instance change ids', JSON.stringify(networkStore.networkInstanceIds))
const insts = networkStore.networkInstanceIds
const no_tun = networkStore.isNoTunEnabled(insts[0])
if (no_tun) {
export async function onNetworkInstanceChange(instanceId: string) {
console.error('vpn service network instance change id', instanceId)
if (!instanceId) {
await doStopVpn()
return
}
if (!insts) {
await doStopVpn()
const config = await getConfig(instanceId)
if (config.no_tun) {
return
}
const curNetworkInfo = networkStore.networkInfos[insts[0]]
const curNetworkInfo = (await collectNetworkInfo(instanceId)).info.map[instanceId]
if (!curNetworkInfo || curNetworkInfo?.error_msg?.length) {
await doStopVpn()
return
@@ -146,7 +141,7 @@ async function onNetworkInstanceChange() {
network_length = 24
}
const routes = getRoutesForVpn(curNetworkInfo?.routes, networkStore.curNetwork)
const routes = getRoutesForVpn(curNetworkInfo?.routes, config)
const ipChanged = virtual_ip !== curVpnStatus.ipv4Addr
const routesChanged = JSON.stringify(routes) !== JSON.stringify(curVpnStatus.routes)
@@ -164,48 +159,25 @@ async function onNetworkInstanceChange() {
await doStartVpn(virtual_ip, 24, routes)
}
catch (e) {
console.error('start vpn service failed, clear all network insts.', e)
networkStore.clearNetworkInstances()
await retainNetworkInstance(networkStore.networkInstanceIds)
console.error('start vpn service failed, stop all other network insts.', e)
await runNetworkInstance(config);
}
}
}
async function watchNetworkInstance() {
let subscribe_running = false
networkStore.$subscribe(async () => {
if (subscribe_running) {
return
}
subscribe_running = true
try {
await onNetworkInstanceChange()
}
catch (_) {
}
subscribe_running = false
})
console.error('vpn service watch network instance')
}
function isNoTunEnabled(instanceId: string | undefined) {
async function isNoTunEnabled(instanceId: string | undefined) {
if (!instanceId) {
return false
}
const no_tun = networkStore.isNoTunEnabled(instanceId)
if (no_tun) {
return true
}
return false
return (await getConfig(instanceId)).no_tun ?? false
}
export async function initMobileVpnService() {
await registerVpnServiceListener()
await watchNetworkInstance()
}
export async function prepareVpnService(instanceId: string) {
if (isNoTunEnabled(instanceId)) {
if (await isNoTunEnabled(instanceId)) {
return
}
console.log('prepare vpn')

View File

@@ -1,45 +0,0 @@
import type { NetworkTypes } from 'easytier-frontend-lib'
import { invoke } from '@tauri-apps/api/core'
type NetworkConfig = NetworkTypes.NetworkConfig
type NetworkInstanceRunningInfo = NetworkTypes.NetworkInstanceRunningInfo
export async function parseNetworkConfig(cfg: NetworkConfig) {
return invoke<string>('parse_network_config', { cfg })
}
export async function generateNetworkConfig(tomlConfig: string) {
return invoke<NetworkConfig>('generate_network_config', { tomlConfig })
}
export async function runNetworkInstance(cfg: NetworkConfig) {
return invoke('run_network_instance', { cfg })
}
export async function retainNetworkInstance(instanceIds: string[]) {
return invoke('retain_network_instance', { instanceIds })
}
export async function collectNetworkInfos() {
return await invoke<Record<string, NetworkInstanceRunningInfo>>('collect_network_infos')
}
export async function getOsHostname() {
return await invoke<string>('get_os_hostname')
}
export async function isAutostart() {
return await invoke<boolean>('is_autostart')
}
export async function setLoggingLevel(level: string) {
return await invoke('set_logging_level', { level })
}
export async function setTunFd(instanceId: string, fd: number) {
return await invoke('set_tun_fd', { instanceId, fd })
}
export async function getEasytierVersion() {
return await invoke<string>('easytier_version')
}

View File

@@ -1,15 +1,15 @@
import Aura from '@primeuix/themes/aura';
import PrimeVue from 'primevue/config'
import ToastService from 'primevue/toastservice'
import PrimeVue from 'primevue/config';
import { createRouter, createWebHistory } from 'vue-router/auto'
import { routes } from 'vue-router/auto-routes'
import App from '~/App.vue'
import EasyTierFrontendLib, { I18nUtils } from 'easytier-frontend-lib'
import EasyTierFrontendLib, { I18nUtils } from 'easytier-frontend-lib';
import { createRouter, createWebHistory } from 'vue-router/auto';
import { routes } from 'vue-router/auto-routes';
import App from '~/App.vue';
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch'
import '~/styles.css'
import 'easytier-frontend-lib/style.css'
import 'easytier-frontend-lib/style.css';
import { ConfirmationService, DialogService, ToastService } from 'primevue';
import '~/styles.css';
import { getAutoLaunchStatusAsync, loadAutoLaunchStatusAsync } from './modules/auto_launch';
if (import.meta.env.PROD) {
document.addEventListener('keydown', (event) => {
@@ -55,7 +55,9 @@ async function main() {
},
},
})
app.use(ToastService as any)
app.use(ToastService)
app.use(DialogService)
app.use(ConfirmationService)
app.mount('#app')
}

View File

@@ -0,0 +1,44 @@
import { type Api, type NetworkTypes } from "easytier-frontend-lib";
import * as backend from "~/composables/backend";
export class GUIRemoteClient implements Api.RemoteClient {
async validate_config(config: NetworkTypes.NetworkConfig): Promise<Api.ValidateConfigResponse> {
return backend.validateConfig(config);
}
async run_network(config: NetworkTypes.NetworkConfig): Promise<undefined> {
await backend.runNetworkInstance(config);
}
async get_network_info(inst_id: string): Promise<NetworkTypes.NetworkInstanceRunningInfo | undefined> {
return backend.collectNetworkInfo(inst_id).then(infos => infos.info.map[inst_id]);
}
async list_network_instance_ids(): Promise<Api.ListNetworkInstanceIdResponse> {
return backend.listNetworkInstanceIds();
}
async delete_network(inst_id: string): Promise<undefined> {
await backend.deleteNetworkInstance(inst_id);
}
async update_network_instance_state(inst_id: string, disabled: boolean): Promise<undefined> {
await backend.updateNetworkConfigState(inst_id, disabled);
}
async save_config(config: NetworkTypes.NetworkConfig): Promise<undefined> {
await backend.saveNetworkConfig(config);
}
async get_network_config(inst_id: string): Promise<NetworkTypes.NetworkConfig> {
return backend.getConfig(inst_id);
}
async generate_config(config: NetworkTypes.NetworkConfig): Promise<Api.GenerateConfigResponse> {
try {
return { toml_config: await backend.parseNetworkConfig(config) };
} catch (e) {
return { error: e + "" };
}
}
async parse_config(toml_config: string): Promise<Api.ParseConfigResponse> {
try {
return { config: await backend.generateNetworkConfig(toml_config) }
} catch (e) {
return { error: e + "" };
}
}
}

View File

@@ -1,148 +1,26 @@
<script setup lang="ts">
import { appLogDir } from '@tauri-apps/api/path'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
import { type } from '@tauri-apps/plugin-os'
import { exit } from '@tauri-apps/plugin-process'
import { open } from '@tauri-apps/plugin-shell'
import TieredMenu from 'primevue/tieredmenu'
import { useToast } from 'primevue/usetoast'
import { NetworkTypes, Config, Status, Utils, I18nUtils, ConfigEditDialog } from 'easytier-frontend-lib'
import { isAutostart, setLoggingLevel } from '~/composables/network'
import { appLogDir } from '@tauri-apps/api/path'
import { writeText } from '@tauri-apps/plugin-clipboard-manager'
import { exit } from '@tauri-apps/plugin-process'
import { I18nUtils, RemoteManagement } from "easytier-frontend-lib"
import type { MenuItem } from 'primevue/menuitem'
import { useTray } from '~/composables/tray'
import { GUIRemoteClient } from '~/modules/api'
import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch'
import { getDockVisibilityStatus, loadDockVisibilityAsync } from '~/modules/dock_visibility'
const { t, locale } = useI18n()
const visible = ref(false)
const aboutVisible = ref(false)
const tomlConfig = ref('')
useTray(true)
const items = ref([
{
label: () => activeStep.value == "2" ? t('show_config') : t('edit_config'),
icon: 'pi pi-file-edit',
command: async () => {
try {
const ret = await parseNetworkConfig(networkStore.curNetwork)
tomlConfig.value = ret
}
catch (e: any) {
tomlConfig.value = e
}
visible.value = true
},
},
{
label: () => t('del_cur_network'),
icon: 'pi pi-times',
command: async () => {
networkStore.removeNetworkInstance(networkStore.curNetwork.instance_id)
await retainNetworkInstance(networkStore.networkInstanceIds)
networkStore.delCurNetwork()
},
disabled: () => networkStore.networkList.length <= 1,
},
])
const remoteClient = computed(() => new GUIRemoteClient());
const instanceId = ref<string | undefined>(undefined);
enum Severity {
None = 'none',
Success = 'success',
Info = 'info',
Warn = 'warn',
Error = 'error',
}
const messageBarSeverity = ref(Severity.None)
const messageBarContent = ref('')
const toast = useToast()
const networkStore = useNetworkStore()
const curNetworkConfig = computed(() => {
if (networkStore.curNetworkId) {
// console.log('instanceId', props.instanceId)
const c = networkStore.networkList.find(n => n.instance_id === networkStore.curNetworkId)
if (c !== undefined)
return c
}
return networkStore.curNetwork
})
const curNetworkInst = computed<NetworkTypes.NetworkInstance | null>(() => {
let ret = networkStore.networkInstances.find(n => n.instance_id === curNetworkConfig.value.instance_id)
console.log('curNetworkInst', ret)
if (ret === undefined) {
return null;
} else {
return ret;
}
})
function addNewNetwork() {
networkStore.addNewNetwork()
networkStore.curNetwork = networkStore.lastNetwork
}
networkStore.$subscribe(async () => {
networkStore.saveToLocalStorage()
try {
await parseNetworkConfig(networkStore.curNetwork)
messageBarSeverity.value = Severity.None
}
catch (e: any) {
messageBarContent.value = e
messageBarSeverity.value = Severity.Error
}
})
async function runNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
if (type() === 'android') {
await prepareVpnService(cfg.instance_id)
networkStore.clearNetworkInstances()
}
else {
networkStore.removeNetworkInstance(cfg.instance_id)
}
await retainNetworkInstance(networkStore.networkInstanceIds)
networkStore.addNetworkInstance(cfg.instance_id)
try {
await runNetworkInstance(cfg)
networkStore.addAutoStartInstId(cfg.instance_id)
}
catch (e: any) {
// console.error(e)
toast.add({ severity: 'info', detail: e })
}
cb()
}
async function stopNetworkCb(cfg: NetworkTypes.NetworkConfig, cb: () => void) {
// console.log('stopNetworkCb', cfg, cb)
cb()
networkStore.removeNetworkInstance(cfg.instance_id)
await retainNetworkInstance(networkStore.networkInstanceIds)
networkStore.removeAutoStartInstId(cfg.instance_id)
}
async function updateNetworkInfos() {
networkStore.updateWithNetworkInfos(await collectNetworkInfos())
}
let intervalId = 0
onMounted(async () => {
intervalId = window.setInterval(async () => {
await updateNetworkInfos()
}, 500)
window.setTimeout(async () => {
await setTrayMenu([
await MenuItemShow(t('tray.show')),
@@ -150,16 +28,47 @@ onMounted(async () => {
])
}, 1000)
})
onUnmounted(() => clearInterval(intervalId))
const activeStep = computed(() => {
return networkStore.networkInstanceIds.includes(networkStore.curNetworkId) ? '2' : '1'
})
let current_log_level = 'off'
const setting_menu = ref()
const setting_menu_items = ref([
const log_menu = ref()
const log_menu_items_popup: Ref<MenuItem[]> = ref([
...['off', 'warn', 'info', 'debug', 'trace'].map(level => ({
label: () => t(`logging_level_${level}`) + (current_log_level === level ? ' ✓' : ''),
command: async () => {
current_log_level = level
await setLoggingLevel(level)
},
})),
{
separator: true,
},
{
label: () => t('logging_open_dir'),
icon: 'pi pi-folder-open',
command: async () => {
// console.log('open log dir', await appLogDir())
await open(await appLogDir())
},
},
{
label: () => t('logging_copy_dir'),
icon: 'pi pi-tablet',
command: async () => {
await writeText(await appLogDir())
},
},
])
function toggle_log_menu(event: any) {
log_menu.value.toggle(event)
}
function getLabel(item: MenuItem) {
return typeof item.label === 'function' ? item.label() : item.label
}
const setting_menu_items: Ref<MenuItem[]> = ref([
{
label: () => t('exchange_language'),
icon: 'pi pi-language',
@@ -187,40 +96,10 @@ const setting_menu_items = ref([
visible: () => type() === 'macos',
},
{
key: 'logging_menu',
label: () => t('logging'),
icon: 'pi pi-file',
items: (function () {
const levels = ['off', 'warn', 'info', 'debug', 'trace']
const items = []
for (const level of levels) {
items.push({
label: () => t(`logging_level_${level}`) + (current_log_level === level ? ' ✓' : ''),
command: async () => {
current_log_level = level
await setLoggingLevel(level)
},
})
}
items.push({
separator: true,
})
items.push({
label: () => t('logging_open_dir'),
icon: 'pi pi-folder-open',
command: async () => {
// console.log('open log dir', await appLogDir())
await open(await appLogDir())
},
})
items.push({
label: () => t('logging_copy_dir'),
icon: 'pi pi-tablet',
command: async () => {
await writeText(await appLogDir())
},
})
return items
})(),
items: [], // Keep this to show it's a parent menu
},
{
label: () => t('about.title'),
@@ -238,25 +117,6 @@ const setting_menu_items = ref([
},
])
function toggle_setting_menu(event: any) {
setting_menu.value.toggle(event)
}
onBeforeMount(async () => {
networkStore.loadFromLocalStorage()
if (type() !== 'android' && getAutoLaunchStatus() && await isAutostart()) {
getCurrentWindow().hide()
const autoStartIds = networkStore.autoStartInstIds
for (const id of autoStartIds) {
const cfg = networkStore.networkList.find((item: NetworkTypes.NetworkConfig) => item.instance_id === id)
if (cfg) {
networkStore.addNetworkInstance(cfg.instance_id)
await runNetworkInstance(cfg)
}
}
}
})
onMounted(async () => {
if (type() === 'android') {
try {
@@ -266,125 +126,37 @@ onMounted(async () => {
console.error("easytier init vpn service failed", e)
}
}
const unlisten = await listenGlobalEvents()
await sendConfigs()
return () => {
unlisten()
}
})
function isRunning(id: string) {
return networkStore.networkInstanceIds.includes(id)
}
async function saveTomlConfig(tomlConfig: string) {
const config = await generateNetworkConfig(tomlConfig)
networkStore.replaceCurNetwork(config);
toast.add({ severity: 'success', detail: t('config_saved'), life: 3000 })
visible.value = false
}
</script>
<script lang="ts">
</script>
<template>
<div id="root" class="flex flex-col">
<ConfigEditDialog v-model:visible="visible" :cur-network="curNetworkConfig" :readonly="activeStep !== '1'"
:save-config="saveTomlConfig" :generate-config="parseNetworkConfig" />
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
<About />
</Dialog>
<Menu ref="log_menu" :model="log_menu_items_popup" :popup="true" />
<div class="w-full">
<div class="flex items-center gap-4 p-4 h-20">
<!-- 网络按钮 -->
<div class="flex shrink-0 items-center">
<Button icon="pi pi-plus" severity="primary" :label="t('add_new_network')" class="hidden md:inline-flex"
@click="addNewNetwork" />
<Button icon="pi pi-plus" severity="primary" class="md:hidden px-6" @click="addNewNetwork" />
</div>
<RemoteManagement class="flex-1 overflow-y-auto" :api="remoteClient" v-bind:instance-id="instanceId" />
<!-- 网络选择 - 占据中间剩余空间 -->
<Select v-model="networkStore.curNetwork" :options="networkStore.networkList" :highlight-on-select="false"
:placeholder="t('select_network')" class="flex-1 h-full min-w-0">
<template #value="slotProps">
<div class="flex items-center content-center min-w-0">
<div class="mr-4 flex-col min-w-0 flex-1">
<span class="truncate block"> &nbsp; {{ slotProps.value.network_name }}</span>
</div>
<Tag class="my-auto leading-3 shrink-0"
:severity="isRunning(slotProps.value.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.value.instance_id) ? 'network_running' : 'network_stopped')" />
</div>
</template>
<template #option="slotProps">
<div class="flex flex-col items-start content-center max-w-full">
<div class="flex items-center min-w-0 w-full">
<div class="mr-4 min-w-0 flex-1">
<span class="truncate block">{{ t('network_name') }}: {{ slotProps.option.network_name }}</span>
</div>
<Tag class="my-auto leading-3 shrink-0"
:severity="isRunning(slotProps.option.instance_id) ? 'success' : 'info'"
:value="t(isRunning(slotProps.option.instance_id) ? 'network_running' : 'network_stopped')" />
</div>
<div v-if="slotProps.option.networking_method !== NetworkTypes.NetworkingMethod.Standalone"
class="max-w-full overflow-hidden text-ellipsis">
{{ slotProps.option.networking_method === NetworkTypes.NetworkingMethod.Manual
? slotProps.option.peer_urls.join(', ')
: slotProps.option.public_server_url }}
</div>
<div
v-if="isRunning(slotProps.option.instance_id) && networkStore.instances[slotProps.option.instance_id].detail && (!!networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)">
{{
Utils.ipv4InetToString(networkStore.instances[slotProps.option.instance_id].detail?.my_node_info.virtual_ipv4)
}}
</div>
</div>
</template>
</Select>
<!-- 设置按钮 -->
<div class="flex items-center shrink-0">
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" :label="t('settings')"
class="hidden md:inline-flex" aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
<Button icon="pi pi-cog" severity="secondary" aria-haspopup="true" class="md:hidden px-6"
aria-controls="overlay_setting_menu" @click="toggle_setting_menu" />
<TieredMenu id="overlay_setting_menu" ref="setting_menu" :model="setting_menu_items" :popup="true" />
</div>
</div>
</div>
<Panel class="h-full overflow-y-auto">
<Stepper :value="activeStep">
<StepList value="1">
<Step value="1">
{{ t('config_network') }}
</Step>
<Step value="2">
{{ t('running') }}
</Step>
</StepList>
<StepPanels value="1">
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="1">
<Config :instance-id="networkStore.curNetworkId" :config-invalid="messageBarSeverity !== Severity.None"
:cur-network="curNetworkConfig" @run-network="runNetworkCb($event, () => activateCallback('2'))" />
</StepPanel>
<StepPanel v-slot="{ activateCallback = (s: string) => { } } = {}" value="2">
<div class="flex flex-col">
<Status :cur-network-inst="curNetworkInst" />
</div>
<div class="flex pt-6 justify-center">
<Button :label="t('stop_network')" severity="danger" icon="pi pi-arrow-left"
@click="stopNetworkCb(networkStore.curNetwork, () => activateCallback('1'))" />
</div>
</StepPanel>
</StepPanels>
</Stepper>
</Panel>
<div>
<Menubar :model="items" breakpoint="300px" />
<InlineMessage v-if="messageBarSeverity !== Severity.None" class="absolute bottom-0 right-0" severity="error">
{{ messageBarContent }}
</InlineMessage>
</div>
<Menubar :model="setting_menu_items" breakpoint="560px">
<template #item="{ item, props }">
<a v-if="item.key === 'logging_menu'" v-bind="props.action" @click="toggle_log_menu">
<span :class="item.icon" />
<span class="p-menubar-item-label">{{ getLabel(item) }}</span>
<span class="pi pi-angle-down p-menubar-item-icon text-[9px]"></span>
</a>
<a v-else v-bind="props.action">
<span :class="item.icon" />
<span class="p-menubar-item-label">{{ getLabel(item) }}</span>
</a>
</template>
</Menubar>
</div>
</template>

View File

@@ -1,148 +0,0 @@
import { NetworkTypes } from 'easytier-frontend-lib'
export const useNetworkStore = defineStore('networkStore', {
state: () => {
const networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
return {
// for initially empty lists
networkList: networkList as NetworkTypes.NetworkConfig[],
// for data that is not yet loaded
curNetwork: networkList[0],
// uuid -> instance
instances: {} as Record<string, NetworkTypes.NetworkInstance>,
networkInfos: {} as Record<string, NetworkTypes.NetworkInstanceRunningInfo>,
autoStartInstIds: [] as string[],
}
},
getters: {
lastNetwork(): NetworkTypes.NetworkConfig {
return this.networkList[this.networkList.length - 1]
},
curNetworkId(): string {
return this.curNetwork.instance_id
},
networkInstances(): Array<NetworkTypes.NetworkInstance> {
return Object.values(this.instances)
},
networkInstanceIds(): Array<string> {
return Object.keys(this.instances)
},
},
actions: {
addNewNetwork() {
this.networkList.push(NetworkTypes.DEFAULT_NETWORK_CONFIG())
},
delCurNetwork() {
const curNetworkIdx = this.networkList.indexOf(this.curNetwork)
this.networkList.splice(curNetworkIdx, 1)
const nextCurNetworkIdx = Math.min(curNetworkIdx, this.networkList.length - 1)
this.curNetwork = this.networkList[nextCurNetworkIdx]
},
replaceCurNetwork(cfg: NetworkTypes.NetworkConfig) {
const curNetworkIdx = this.networkList.indexOf(this.curNetwork)
this.networkList[curNetworkIdx] = cfg
this.curNetwork = cfg
},
removeNetworkInstance(instanceId: string) {
delete this.instances[instanceId]
},
addNetworkInstance(instanceId: string) {
this.instances[instanceId] = {
instance_id: instanceId,
running: false,
error_msg: '',
detail: undefined,
}
},
clearNetworkInstances() {
this.instances = {}
},
updateWithNetworkInfos(networkInfos: Record<string, NetworkTypes.NetworkInstanceRunningInfo>) {
this.networkInfos = networkInfos
for (const [instanceId, info] of Object.entries(networkInfos)) {
if (this.instances[instanceId] === undefined)
this.addNetworkInstance(instanceId)
this.instances[instanceId].running = info.running
this.instances[instanceId].error_msg = info.error_msg || ''
this.instances[instanceId].detail = info
}
},
loadFromLocalStorage() {
let networkList: NetworkTypes.NetworkConfig[]
// if localStorage default is [{}], instanceId will be undefined
networkList = JSON.parse(localStorage.getItem('networkList') || '[]')
networkList = networkList.map((cfg) => {
return { ...NetworkTypes.DEFAULT_NETWORK_CONFIG(), ...cfg } as NetworkTypes.NetworkConfig
})
// prevent a empty list from localStorage, should not happen
if (networkList.length === 0)
networkList = [NetworkTypes.DEFAULT_NETWORK_CONFIG()]
this.networkList = networkList
this.curNetwork = this.networkList[0]
this.loadAutoStartInstIdsFromLocalStorage()
},
saveToLocalStorage() {
localStorage.setItem('networkList', JSON.stringify(this.networkList))
},
saveAutoStartInstIdsToLocalStorage() {
localStorage.setItem('autoStartInstIds', JSON.stringify(this.autoStartInstIds))
},
loadAutoStartInstIdsFromLocalStorage() {
try {
this.autoStartInstIds = JSON.parse(localStorage.getItem('autoStartInstIds') || '[]')
}
catch (e) {
console.error(e)
this.autoStartInstIds = []
}
},
addAutoStartInstId(instanceId: string) {
if (!this.autoStartInstIds.includes(instanceId)) {
this.autoStartInstIds.push(instanceId)
}
this.saveAutoStartInstIdsToLocalStorage()
},
removeAutoStartInstId(instanceId: string) {
const idx = this.autoStartInstIds.indexOf(instanceId)
if (idx !== -1) {
this.autoStartInstIds.splice(idx, 1)
}
this.saveAutoStartInstIdsToLocalStorage()
},
isNoTunEnabled(instanceId: string): boolean {
const cfg = this.networkList.find((cfg) => cfg.instance_id === instanceId)
if (!cfg)
return false
return cfg.no_tun ?? false
},
},
})
if (import.meta.hot)
import.meta.hot.accept(acceptHMRUpdate(useNetworkStore as any, import.meta.hot))