mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-07 02:09:06 +00:00
feat(gui): add service and remote mode support (#1578)
This PR fundamentally restructures the EasyTier GUI, introducing support for service mode and remote mode, transforming it from a simple desktop application into a powerful network management terminal. This change allows users to persistently run the EasyTier core as a background service or remotely manage multiple EasyTier instances, greatly improving deployment flexibility and manageability.
This commit is contained in:
Generated
+12
-36
@@ -491,17 +491,6 @@ version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
|
||||
|
||||
[[package]]
|
||||
name = "auto-launch"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471"
|
||||
dependencies = [
|
||||
"dirs 4.0.0",
|
||||
"thiserror 1.0.63",
|
||||
"winreg 0.10.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "auto_impl"
|
||||
version = "1.2.1"
|
||||
@@ -2256,7 +2245,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
"tauri-plugin-autostart",
|
||||
"tauri-plugin-clipboard-manager",
|
||||
"tauri-plugin-os",
|
||||
"tauri-plugin-positioner",
|
||||
@@ -7703,13 +7691,14 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "service-manager"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/chipsenkbeil/service-manager-rs.git?branch=main#0294d3b9769c8ef7db8b4e831fb1c4f14b7d473b"
|
||||
source = "git+https://github.com/EasyTier/service-manager-rs.git?branch=main#5eb28f7a686858eea4f4933534ed989d3b71dc2a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dirs 4.0.0",
|
||||
"encoding-utils",
|
||||
"encoding_rs",
|
||||
"plist",
|
||||
"sys-info",
|
||||
"which 4.4.2",
|
||||
"xml-rs",
|
||||
]
|
||||
@@ -8319,6 +8308,16 @@ dependencies = [
|
||||
"syn 2.0.87",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-info"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.1"
|
||||
@@ -8584,20 +8583,6 @@ dependencies = [
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-autostart"
|
||||
version = "2.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "062cdcd483d5e3148c9a64dabf8c574e239e2aa1193cf208d95cf89a676f87a5"
|
||||
dependencies = [
|
||||
"auto-launch",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-clipboard-manager"
|
||||
version = "2.3.0"
|
||||
@@ -10891,15 +10876,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.10.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winreg"
|
||||
version = "0.50.0"
|
||||
|
||||
+12
-1
@@ -3882,13 +3882,14 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "service-manager"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/chipsenkbeil/service-manager-rs.git?branch=main#0294d3b9769c8ef7db8b4e831fb1c4f14b7d473b"
|
||||
source = "git+https://github.com/EasyTier/service-manager-rs.git?branch=main#5eb28f7a686858eea4f4933534ed989d3b71dc2a"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"dirs",
|
||||
"encoding-utils",
|
||||
"encoding_rs",
|
||||
"plist",
|
||||
"sys-info",
|
||||
"which 4.4.2",
|
||||
"xml-rs",
|
||||
]
|
||||
@@ -4071,6 +4072,16 @@ dependencies = [
|
||||
"syn 2.0.106",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-info"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sys-locale"
|
||||
version = "0.3.2"
|
||||
|
||||
@@ -50,7 +50,7 @@ tauri-plugin-clipboard-manager = "2.3.0"
|
||||
tauri-plugin-positioner = { version = "2.3.0", features = ["tray-icon"] }
|
||||
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"
|
||||
|
||||
|
||||
@@ -45,10 +45,6 @@
|
||||
"os:allow-arch",
|
||||
"os:allow-hostname",
|
||||
"os:allow-platform",
|
||||
"os:allow-locale",
|
||||
"autostart:default",
|
||||
"autostart:allow-disable",
|
||||
"autostart:allow-enable",
|
||||
"autostart:allow-is-enabled"
|
||||
"os:allow-locale"
|
||||
]
|
||||
}
|
||||
+385
-155
@@ -3,6 +3,7 @@
|
||||
|
||||
mod elevate;
|
||||
|
||||
use anyhow::Context;
|
||||
use easytier::proto::api::manage::{
|
||||
CollectNetworkInfoResponse, ValidateConfigResponse, WebClientService,
|
||||
WebClientServiceClientFactory,
|
||||
@@ -17,10 +18,11 @@ use easytier::{
|
||||
launcher::NetworkConfig,
|
||||
rpc_service::ApiRpcServer,
|
||||
tunnel::ring::RingTunnelListener,
|
||||
utils::{self, NewFilterSender},
|
||||
utils::{self},
|
||||
};
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
use uuid::Uuid;
|
||||
|
||||
use tauri::{AppHandle, Emitter, Manager as _};
|
||||
@@ -28,19 +30,27 @@ use tauri::{AppHandle, Emitter, Manager as _};
|
||||
#[cfg(not(target_os = "android"))]
|
||||
use tauri::tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent};
|
||||
|
||||
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 INSTANCE_MANAGER: once_cell::sync::Lazy<RwLock<Option<Arc<NetworkInstanceManager>>>> =
|
||||
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::OnceCell<manager::GUIClientManager> =
|
||||
once_cell::sync::OnceCell::new();
|
||||
static CLIENT_MANAGER: once_cell::sync::Lazy<RwLock<Option<manager::GUIClientManager>>> =
|
||||
once_cell::sync::Lazy::new(|| RwLock::new(None));
|
||||
|
||||
static RING_RPC_SERVER: once_cell::sync::Lazy<RwLock<Option<ApiRpcServer<RingTunnelListener>>>> =
|
||||
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> {
|
||||
@@ -88,19 +98,17 @@ async fn run_network_instance(
|
||||
app.emit("pre_run_network_instance", cfg.instance_id())
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let client_manager = get_client_manager!()?;
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
if cfg.no_tun() == false {
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
client_manager
|
||||
.disable_instances_with_tun(&app)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
client_manager
|
||||
.handle_run_network_instance(app.clone(), cfg, save)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -118,31 +126,32 @@ async fn collect_network_info(
|
||||
let instance_id = instance_id
|
||||
.parse()
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
get_client_manager!()?
|
||||
.handle_collect_network_info(app, Some(vec![instance_id]))
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_logging_level(level: String) -> Result<(), String> {
|
||||
#[allow(static_mut_refs)]
|
||||
let sender = unsafe { LOGGER_LEVEL_SENDER.as_ref().unwrap() };
|
||||
sender.send(level).map_err(|e| e.to_string())?;
|
||||
async fn set_logging_level(level: String) -> Result<(), String> {
|
||||
println!("Setting logging level to: {}", level);
|
||||
get_client_manager!()?
|
||||
.set_logging_level(level.clone())
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn set_tun_fd(fd: i32) -> Result<(), String> {
|
||||
if let Some(uuid) = CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
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
|
||||
instance_manager
|
||||
.set_tun_fd(&uuid, fd)
|
||||
.map_err(|e| e.to_string())?;
|
||||
}
|
||||
@@ -153,9 +162,7 @@ fn set_tun_fd(fd: i32) -> Result<(), String> {
|
||||
async fn list_network_instance_ids(
|
||||
app: AppHandle,
|
||||
) -> Result<ListNetworkInstanceIdsJsonResp, String> {
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
get_client_manager!()?
|
||||
.handle_list_network_instance_ids(app)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
@@ -166,16 +173,12 @@ async fn remove_network_instance(app: AppHandle, instance_id: String) -> Result<
|
||||
let instance_id = instance_id
|
||||
.parse()
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
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
|
||||
.get()
|
||||
.unwrap()
|
||||
.notify_vpn_stop_if_no_tun(&app)?;
|
||||
client_manager.notify_vpn_stop_if_no_tun(&app)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -188,17 +191,13 @@ async fn update_network_config_state(
|
||||
let instance_id = instance_id
|
||||
.parse()
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
let client_manager = get_client_manager!()?;
|
||||
client_manager
|
||||
.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)?;
|
||||
client_manager.notify_vpn_stop_if_no_tun(&app)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -209,9 +208,7 @@ async fn save_network_config(app: AppHandle, cfg: NetworkConfig) -> Result<(), S
|
||||
.instance_id()
|
||||
.parse()
|
||||
.map_err(|e: uuid::Error| e.to_string())?;
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
get_client_manager!()?
|
||||
.handle_save_network_config(app, instance_id, cfg)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
@@ -222,9 +219,7 @@ async fn validate_config(
|
||||
app: AppHandle,
|
||||
config: NetworkConfig,
|
||||
) -> Result<ValidateConfigResponse, String> {
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
get_client_manager!()?
|
||||
.handle_validate_config(app, config)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
@@ -232,9 +227,7 @@ async fn validate_config(
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_config(app: AppHandle, instance_id: String) -> Result<NetworkConfig, String> {
|
||||
let cfg = CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
let cfg = get_client_manager!()?
|
||||
.storage
|
||||
.get_network_config(app, &instance_id)
|
||||
.await
|
||||
@@ -249,10 +242,7 @@ async fn load_configs(
|
||||
configs: Vec<NetworkConfig>,
|
||||
enabled_networks: Vec<String>,
|
||||
) -> Result<(), String> {
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.storage
|
||||
get_client_manager!()?
|
||||
.load_configs(app, configs, enabled_networks)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
@@ -264,14 +254,143 @@ async fn get_network_metas(
|
||||
app: AppHandle,
|
||||
instance_ids: Vec<uuid::Uuid>,
|
||||
) -> Result<GetNetworkMetasResponse, String> {
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn init_rpc_connection(_app: AppHandle, 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 ring_rpc_server_guard = RING_RPC_SERVER
|
||||
.try_write()
|
||||
.map_err(|_| "Failed to acquire write lock for ring rpc server")?;
|
||||
|
||||
let normal_mode = url.is_none();
|
||||
if normal_mode {
|
||||
let instance_manager = if let Some(im) = instance_manager_guard.take() {
|
||||
im
|
||||
} else {
|
||||
Arc::new(NetworkInstanceManager::new())
|
||||
};
|
||||
let rpc_server = if let Some(rpc_server) = ring_rpc_server_guard.take() {
|
||||
rpc_server
|
||||
} else {
|
||||
ApiRpcServer::from_tunnel(
|
||||
RingTunnelListener::new(
|
||||
format!("ring://{}", RPC_RING_UUID.deref()).parse().unwrap(),
|
||||
),
|
||||
instance_manager.clone(),
|
||||
)
|
||||
.with_rx_timeout(None)
|
||||
.serve()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
};
|
||||
|
||||
*instance_manager_guard = Some(instance_manager);
|
||||
*ring_rpc_server_guard = Some(rpc_server);
|
||||
} else {
|
||||
*ring_rpc_server_guard = None;
|
||||
}
|
||||
|
||||
let mut client_manager = tokio::time::timeout(
|
||||
std::time::Duration::from_millis(1000),
|
||||
manager::GUIClientManager::new(url),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| "connect remote rpc timed out".to_string())?
|
||||
.with_context(|| "Failed to connect remote rpc")
|
||||
.map_err(|e| format!("{:#}", e))?;
|
||||
if let Some(old_manager) = client_manager_guard.take() {
|
||||
client_manager.storage = old_manager.storage;
|
||||
}
|
||||
*client_manager_guard = Some(client_manager);
|
||||
|
||||
if !normal_mode {
|
||||
if let Some(instance_manager) = instance_manager_guard.take() {
|
||||
instance_manager
|
||||
.retain_network_instance(vec![])
|
||||
.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())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
@@ -289,19 +408,23 @@ fn toggle_window_visibility<R: tauri::Runtime>(app: &tauri::AppHandle<R>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_exe_path() -> String {
|
||||
if let Ok(appimage_path) = std::env::var("APPIMAGE") {
|
||||
if !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 = std::env::var("APPIMAGE")
|
||||
.ok()
|
||||
.or_else(|| std::env::args().next())
|
||||
.unwrap_or_default();
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let mut stdcmd = std::process::Command::new(&exe_path);
|
||||
if args.contains(&AUTOSTART_ARG.to_owned()) {
|
||||
stdcmd.arg(AUTOSTART_ARG);
|
||||
}
|
||||
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");
|
||||
@@ -313,10 +436,15 @@ mod manager {
|
||||
use super::*;
|
||||
use async_trait::async_trait;
|
||||
use dashmap::{DashMap, DashSet};
|
||||
use easytier::common::global_ctx::GlobalCtx;
|
||||
use easytier::common::stun::MockStunInfoCollector;
|
||||
use easytier::launcher::NetworkConfig;
|
||||
use easytier::proto::api::manage::RunNetworkInstanceRequest;
|
||||
use easytier::proto::api::logger::{LoggerRpc, LoggerRpcClientFactory, SetLoggerConfigRequest};
|
||||
use easytier::proto::api::manage::{ListNetworkInstanceRequest, RunNetworkInstanceRequest};
|
||||
use easytier::proto::common::NatType;
|
||||
use easytier::proto::rpc_impl::bidirect::BidirectRpcManager;
|
||||
use easytier::proto::rpc_types::controller::BaseController;
|
||||
use easytier::rpc_service::logger::LoggerRpcService;
|
||||
use easytier::rpc_service::remote_client::PersistentConfig;
|
||||
use easytier::tunnel::ring::RingTunnelConnector;
|
||||
use easytier::tunnel::TunnelConnector;
|
||||
@@ -344,54 +472,6 @@ mod manager {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn load_configs(
|
||||
&self,
|
||||
app: AppHandle,
|
||||
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.iter().for_each(|v| {
|
||||
self.enabled_networks.insert(*v.key());
|
||||
});
|
||||
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.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("Config not found"))?;
|
||||
CLIENT_MANAGER
|
||||
.get()
|
||||
.unwrap()
|
||||
.get_rpc_client(app.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?
|
||||
.run_network_instance(
|
||||
BaseController::default(),
|
||||
RunNetworkInstanceRequest {
|
||||
inst_id: None,
|
||||
config: Some(config),
|
||||
overwrite: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.enabled_networks.insert(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn save_configs(&self, app: &AppHandle) -> anyhow::Result<()> {
|
||||
let configs: Result<Vec<String>, _> = self
|
||||
.network_configs
|
||||
@@ -507,14 +587,32 @@ mod manager {
|
||||
|
||||
pub(super) struct GUIClientManager {
|
||||
pub(super) storage: GUIStorage,
|
||||
rpc_manager: BidirectRpcManager,
|
||||
pub(super) rpc_manager: BidirectRpcManager,
|
||||
}
|
||||
impl GUIClientManager {
|
||||
pub async fn new() -> Result<Self, anyhow::Error> {
|
||||
pub async fn new(rpc_url: Option<String>) -> Result<Self, anyhow::Error> {
|
||||
let global_ctx = Arc::new(GlobalCtx::new(TomlConfigLoader::default()));
|
||||
global_ctx.replace_stun_info_collector(Box::new(MockStunInfoCollector {
|
||||
udp_nat_type: NatType::Unknown,
|
||||
}));
|
||||
let mut flags = global_ctx.get_flags();
|
||||
flags.bind_device = false;
|
||||
global_ctx.set_flags(flags);
|
||||
let tunnel = if let Some(url) = rpc_url {
|
||||
let mut connector = easytier::connector::create_connector_by_url(
|
||||
&url,
|
||||
&global_ctx,
|
||||
easytier::tunnel::IpVersion::Both,
|
||||
)
|
||||
.await?;
|
||||
connector.connect().await?
|
||||
} else {
|
||||
let mut connector = RingTunnelConnector::new(
|
||||
format!("ring://{}", RPC_RING_UUID.deref()).parse().unwrap(),
|
||||
);
|
||||
let tunnel = connector.connect().await?;
|
||||
connector.connect().await?
|
||||
};
|
||||
|
||||
let rpc_manager = BidirectRpcManager::new();
|
||||
rpc_manager.run_with_tunnel(tunnel);
|
||||
|
||||
@@ -554,6 +652,84 @@ mod manager {
|
||||
}
|
||||
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: LoggerRpcService::string_to_log_level(&level).into(),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn load_configs(
|
||||
&self,
|
||||
app: AppHandle,
|
||||
configs: Vec<NetworkConfig>,
|
||||
enabled_networks: Vec<String>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.storage.network_configs.clear();
|
||||
for cfg in configs {
|
||||
let instance_id = cfg.instance_id();
|
||||
self.storage.network_configs.insert(
|
||||
instance_id.parse()?,
|
||||
GUIConfig(instance_id.to_string(), cfg),
|
||||
);
|
||||
}
|
||||
|
||||
self.storage.enabled_networks.clear();
|
||||
let client = self
|
||||
.get_rpc_client(app.clone())
|
||||
.ok_or_else(|| anyhow::anyhow!("RPC client not found"))?;
|
||||
let running_instances = client
|
||||
.list_network_instance(BaseController::default(), ListNetworkInstanceRequest {})
|
||||
.await?;
|
||||
for id in running_instances.inst_ids {
|
||||
self.storage.enabled_networks.insert(id.into());
|
||||
}
|
||||
for id in enabled_networks {
|
||||
if let Ok(uuid) = id.parse() {
|
||||
if !self.storage.enabled_networks.contains(&uuid) {
|
||||
let config = self
|
||||
.storage
|
||||
.network_configs
|
||||
.get(&uuid)
|
||||
.map(|i| i.value().1.clone());
|
||||
if config.is_none() {
|
||||
continue;
|
||||
}
|
||||
client
|
||||
.run_network_instance(
|
||||
BaseController::default(),
|
||||
RunNetworkInstanceRequest {
|
||||
inst_id: None,
|
||||
config,
|
||||
overwrite: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.storage.enabled_networks.insert(uuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
impl RemoteClientManager<AppHandle, GUIConfig, anyhow::Error> for GUIClientManager {
|
||||
fn get_rpc_client(
|
||||
@@ -577,8 +753,78 @@ mod manager {
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
}
|
||||
impl ServiceOptions {
|
||||
fn to_args_vec(&self) -> Vec<std::ffi::OsString> {
|
||||
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(),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
pub fn run_gui() -> std::process::ExitCode {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
if !check_sudo() {
|
||||
use std::process;
|
||||
@@ -587,35 +833,8 @@ 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"))]
|
||||
{
|
||||
use tauri_plugin_autostart::MacosLauncher;
|
||||
builder = builder.plugin(tauri_plugin_autostart::init(
|
||||
MacosLauncher::LaunchAgent,
|
||||
Some(vec![AUTOSTART_ARG]),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "ios")))]
|
||||
{
|
||||
builder = builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||
@@ -651,13 +870,9 @@ pub fn run() {
|
||||
})
|
||||
.build()
|
||||
.map_err(|e| e.to_string())?;
|
||||
let Ok(Some(logger_reinit)) = utils::init_logger(&config, true) else {
|
||||
let Ok(_) = utils::init_logger(&config, true) else {
|
||||
return Ok(());
|
||||
};
|
||||
#[allow(static_mut_refs)]
|
||||
unsafe {
|
||||
LOGGER_LEVEL_SENDER.replace(logger_reinit)
|
||||
};
|
||||
|
||||
// for tray icon, menu need to be built in js
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -699,6 +914,11 @@ pub fn run() {
|
||||
get_config,
|
||||
load_configs,
|
||||
get_network_metas,
|
||||
init_service,
|
||||
set_service_status,
|
||||
get_service_status,
|
||||
init_rpc_connection,
|
||||
is_client_running,
|
||||
])
|
||||
.on_window_event(|_win, event| match event {
|
||||
#[cfg(not(target_os = "android"))]
|
||||
@@ -724,4 +944,14 @@ pub fn run() {
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
app_lib::run();
|
||||
fn main() -> std::process::ExitCode {
|
||||
if std::env::args().count() > 1 {
|
||||
app_lib::run_cli()
|
||||
} else {
|
||||
app_lib::run_gui()
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+14
@@ -28,15 +28,20 @@ declare global {
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const getEasytierVersion: typeof import('./composables/backend')['getEasytierVersion']
|
||||
const getNetworkMetas: typeof import('./composables/backend')['getNetworkMetas']
|
||||
const getServiceStatus: typeof import('./composables/backend')['getServiceStatus']
|
||||
const h: typeof import('vue')['h']
|
||||
const initMobileVpnService: typeof import('./composables/mobile_vpn')['initMobileVpnService']
|
||||
const initRpcConnection: typeof import('./composables/backend')['initRpcConnection']
|
||||
const initService: typeof import('./composables/backend')['initService']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isClientRunning: typeof import('./composables/backend')['isClientRunning']
|
||||
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 loadMode: typeof import('./composables/mode')['loadMode']
|
||||
const mapActions: typeof import('pinia')['mapActions']
|
||||
const mapGetters: typeof import('pinia')['mapGetters']
|
||||
const mapState: typeof import('pinia')['mapState']
|
||||
@@ -69,11 +74,13 @@ declare global {
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const runNetworkInstance: typeof import('./composables/backend')['runNetworkInstance']
|
||||
const saveMode: typeof import('./composables/mode')['saveMode']
|
||||
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/backend')['setLoggingLevel']
|
||||
const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix']
|
||||
const setServiceStatus: typeof import('./composables/backend')['setServiceStatus']
|
||||
const setTrayMenu: typeof import('./composables/tray')['setTrayMenu']
|
||||
const setTrayRunState: typeof import('./composables/tray')['setTrayRunState']
|
||||
const setTrayTooltip: typeof import('./composables/tray')['setTrayTooltip']
|
||||
@@ -141,15 +148,20 @@ declare module 'vue' {
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getEasytierVersion: UnwrapRef<typeof import('./composables/backend')['getEasytierVersion']>
|
||||
readonly getNetworkMetas: UnwrapRef<typeof import('./composables/backend')['getNetworkMetas']>
|
||||
readonly getServiceStatus: UnwrapRef<typeof import('./composables/backend')['getServiceStatus']>
|
||||
readonly h: UnwrapRef<typeof import('vue')['h']>
|
||||
readonly initMobileVpnService: UnwrapRef<typeof import('./composables/mobile_vpn')['initMobileVpnService']>
|
||||
readonly initRpcConnection: UnwrapRef<typeof import('./composables/backend')['initRpcConnection']>
|
||||
readonly initService: UnwrapRef<typeof import('./composables/backend')['initService']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly isClientRunning: UnwrapRef<typeof import('./composables/backend')['isClientRunning']>
|
||||
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 loadMode: UnwrapRef<typeof import('./composables/mode')['loadMode']>
|
||||
readonly mapActions: UnwrapRef<typeof import('pinia')['mapActions']>
|
||||
readonly mapGetters: UnwrapRef<typeof import('pinia')['mapGetters']>
|
||||
readonly mapState: UnwrapRef<typeof import('pinia')['mapState']>
|
||||
@@ -182,11 +194,13 @@ declare module 'vue' {
|
||||
readonly ref: UnwrapRef<typeof import('vue')['ref']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly runNetworkInstance: UnwrapRef<typeof import('./composables/backend')['runNetworkInstance']>
|
||||
readonly saveMode: UnwrapRef<typeof import('./composables/mode')['saveMode']>
|
||||
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/backend')['setLoggingLevel']>
|
||||
readonly setMapStoreSuffix: UnwrapRef<typeof import('pinia')['setMapStoreSuffix']>
|
||||
readonly setServiceStatus: UnwrapRef<typeof import('./composables/backend')['setServiceStatus']>
|
||||
readonly setTrayMenu: UnwrapRef<typeof import('./composables/tray')['setTrayMenu']>
|
||||
readonly setTrayRunState: UnwrapRef<typeof import('./composables/tray')['setTrayRunState']>
|
||||
readonly setTrayTooltip: UnwrapRef<typeof import('./composables/tray')['setTrayTooltip']>
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch, onMounted, ref } from 'vue';
|
||||
import type { Mode, ServiceMode, RemoteMode } from '~/composables/mode';
|
||||
import { appConfigDir, appLogDir } from '@tauri-apps/api/path';
|
||||
import { join } from '@tauri-apps/api/path';
|
||||
import { getServiceStatus, type ServiceStatus } from '~/composables/backend';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const model = defineModel<Mode>({ required: true })
|
||||
const emit = defineEmits(['uninstall-service', 'stop-service'])
|
||||
|
||||
const defaultConfigDir = ref('')
|
||||
const defaultLogDir = ref('')
|
||||
const serviceStatus = ref<ServiceStatus>('NotInstalled')
|
||||
const isServiceStatusLoaded = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
defaultConfigDir.value = await join(await appConfigDir(), 'config.d')
|
||||
defaultLogDir.value = await appLogDir()
|
||||
})
|
||||
|
||||
const modeOptions = computed(() => [
|
||||
{ label: t('mode.normal'), value: 'normal' },
|
||||
{ label: t('mode.service'), value: 'service' },
|
||||
{ label: t('mode.remote'), value: 'remote' },
|
||||
]);
|
||||
|
||||
const serviceMode = computed({
|
||||
get: () => model.value.mode === 'service' ? model.value as ServiceMode : undefined,
|
||||
set: (value) => {
|
||||
if (value) {
|
||||
model.value = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const remoteMode = computed({
|
||||
get: () => model.value.mode === 'remote' ? model.value as RemoteMode : undefined,
|
||||
set: (value) => {
|
||||
if (value) {
|
||||
model.value = value
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const statusColorClass = computed(() => {
|
||||
switch (serviceStatus.value) {
|
||||
case 'Running':
|
||||
return 'text-green-600'
|
||||
case 'Stopped':
|
||||
return 'text-orange-600'
|
||||
case 'NotInstalled':
|
||||
return 'text-gray-600'
|
||||
default:
|
||||
return 'text-gray-600'
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => model.value.mode, async (newMode, oldMode) => {
|
||||
if (newMode === oldMode)
|
||||
return
|
||||
|
||||
if (newMode === 'service' && !isServiceStatusLoaded.value) {
|
||||
serviceStatus.value = await getServiceStatus()
|
||||
isServiceStatusLoaded.value = true
|
||||
}
|
||||
|
||||
const oldModelValue = { ...model.value }
|
||||
|
||||
if (newMode === 'normal') {
|
||||
model.value = {
|
||||
...oldModelValue,
|
||||
mode: 'normal',
|
||||
}
|
||||
}
|
||||
else if (newMode === 'service') {
|
||||
model.value = {
|
||||
...oldModelValue,
|
||||
mode: 'service',
|
||||
config_dir: serviceMode.value?.config_dir || defaultConfigDir.value,
|
||||
rpc_portal: serviceMode.value?.rpc_portal || '127.0.0.1:15999',
|
||||
file_log_level: serviceMode.value?.file_log_level || 'off',
|
||||
file_log_dir: serviceMode.value?.file_log_dir || defaultLogDir.value,
|
||||
}
|
||||
}
|
||||
else if (newMode === 'remote') {
|
||||
model.value = {
|
||||
...oldModelValue,
|
||||
mode: 'remote',
|
||||
remote_rpc_address: remoteMode.value?.remote_rpc_address || 'tcp://127.0.0.1:15999',
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div>
|
||||
<SelectButton id="mode-select" v-model="model.mode" :options="modeOptions" option-label="label"
|
||||
option-value="value" fluid />
|
||||
</div>
|
||||
|
||||
<!-- Mode descriptions -->
|
||||
<div v-if="model.mode === 'normal'" class="text-sm text-gray-500">
|
||||
{{ t('mode.normal_description') }}
|
||||
</div>
|
||||
<div v-else-if="model.mode === 'service'" class="text-sm text-gray-500">
|
||||
{{ t('mode.service_description') }}
|
||||
</div>
|
||||
<div v-else-if="model.mode === 'remote'" class="text-sm text-gray-500">
|
||||
{{ t('mode.remote_description') }}
|
||||
</div>
|
||||
|
||||
<div v-if="serviceMode" class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="config-dir">{{ t('mode.config_dir') }}</label>
|
||||
<InputText id="config-dir" v-model="serviceMode.config_dir" class="flex-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="rpc-portal">{{ t('mode.rpc_portal') }}</label>
|
||||
<InputText id="rpc-portal" v-model="serviceMode.rpc_portal" class="flex-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="log-level">{{ t('mode.log_level') }}</label>
|
||||
<Select id="log-level" v-model="serviceMode.file_log_level"
|
||||
:options="['off', 'warn', 'info', 'debug', 'trace']" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="log-dir">{{ t('mode.log_dir') }}</label>
|
||||
<InputText id="log-dir" v-model="serviceMode.file_log_dir" class="flex-1" />
|
||||
</div>
|
||||
<div class="flex items-center gap-2 justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<label>{{ t('mode.service_status') }}</label>
|
||||
<span :class="statusColorClass">{{ t(`mode.service_status_${serviceStatus.toLowerCase()}`) }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button :label="t('mode.stop_service')" icon="pi pi-stop-circle" v-if="serviceStatus === 'Running'"
|
||||
@click="emit('stop-service')" severity="warn" text />
|
||||
<Button :label="t('mode.uninstall_service')" icon="pi pi-trash" v-if="serviceStatus !== 'NotInstalled'"
|
||||
@click="emit('uninstall-service')" severity="danger" text />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="remoteMode" class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<label for="remote-addr">{{ t('mode.remote_rpc_address') }}</label>
|
||||
<InputText id="remote-addr" v-model="remoteMode.remote_rpc_address" class="flex-1" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,11 +1,19 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Api, type NetworkTypes } from 'easytier-frontend-lib'
|
||||
import { GetNetworkMetasResponse } from 'node_modules/easytier-frontend-lib/dist/modules/api'
|
||||
import { getAutoLaunchStatusAsync } from '~/modules/auto_launch'
|
||||
|
||||
|
||||
type NetworkConfig = NetworkTypes.NetworkConfig
|
||||
type ValidateConfigResponse = Api.ValidateConfigResponse
|
||||
type ListNetworkInstanceIdResponse = Api.ListNetworkInstanceIdResponse
|
||||
interface ServiceOptions {
|
||||
config_dir: string
|
||||
rpc_portal: string
|
||||
file_log_level: string
|
||||
file_log_dir: string
|
||||
}
|
||||
|
||||
export type ServiceStatus = "Running" | "Stopped" | "NotInstalled"
|
||||
|
||||
export async function parseNetworkConfig(cfg: NetworkConfig) {
|
||||
return invoke<string>('parse_network_config', { cfg })
|
||||
@@ -61,10 +69,29 @@ export async function getConfig(instanceId: string) {
|
||||
|
||||
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 })
|
||||
return await invoke('load_configs', { configs: networkList, enabledNetworks: [] })
|
||||
}
|
||||
|
||||
export async function getNetworkMetas(instanceIds: string[]) {
|
||||
return await invoke<GetNetworkMetasResponse>('get_network_metas', { instanceIds })
|
||||
}
|
||||
|
||||
export async function initService(opts?: ServiceOptions) {
|
||||
return await invoke('init_service', { opts })
|
||||
}
|
||||
|
||||
export async function setServiceStatus(enable: boolean) {
|
||||
return await invoke('set_service_status', { enable })
|
||||
}
|
||||
|
||||
export async function getServiceStatus() {
|
||||
return await invoke<ServiceStatus>('get_service_status')
|
||||
}
|
||||
|
||||
export async function initRpcConnection(url?: string) {
|
||||
return await invoke('init_rpc_connection', { url })
|
||||
}
|
||||
|
||||
export async function isClientRunning() {
|
||||
return await invoke<boolean>('is_client_running')
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ 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',
|
||||
@@ -15,11 +14,6 @@ function onSaveConfigs(event: Event<NetworkTypes.NetworkConfig[]>) {
|
||||
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);
|
||||
@@ -39,7 +33,6 @@ async function onVpnServiceStop(event: Event<string>) {
|
||||
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),
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
interface NormalMode {
|
||||
mode: 'normal'
|
||||
}
|
||||
|
||||
export interface ServiceMode {
|
||||
mode: 'service'
|
||||
config_dir: string
|
||||
rpc_portal: string
|
||||
file_log_level: 'off' | 'warn' | 'info' | 'debug' | 'trace'
|
||||
file_log_dir: string
|
||||
}
|
||||
|
||||
export interface RemoteMode {
|
||||
mode: 'remote'
|
||||
remote_rpc_address: string
|
||||
}
|
||||
|
||||
export function saveMode(mode: Mode) {
|
||||
localStorage.setItem('app_mode', JSON.stringify(mode))
|
||||
}
|
||||
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
|
||||
export function loadMode(): Mode {
|
||||
if (type() === 'android') {
|
||||
return { mode: 'normal' };
|
||||
}
|
||||
const modeStr = localStorage.getItem('app_mode')
|
||||
if (modeStr) {
|
||||
return JSON.parse(modeStr) as Mode
|
||||
} else {
|
||||
return { mode: 'normal' }
|
||||
}
|
||||
}
|
||||
|
||||
export type Mode = NormalMode | ServiceMode | RemoteMode
|
||||
@@ -9,7 +9,7 @@ import App from '~/App.vue';
|
||||
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) => {
|
||||
@@ -29,7 +29,6 @@ if (import.meta.env.PROD) {
|
||||
|
||||
async function main() {
|
||||
await I18nUtils.loadLanguageAsync(localStorage.getItem('lang') || 'en')
|
||||
await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync())
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { disable, enable, isEnabled } from '@tauri-apps/plugin-autostart'
|
||||
|
||||
export async function loadAutoLaunchStatusAsync(target_enable: boolean): Promise<boolean> {
|
||||
try {
|
||||
if (target_enable) {
|
||||
await enable()
|
||||
}
|
||||
else {
|
||||
// 消除没有配置自启动时进行关闭操作报错
|
||||
try {
|
||||
await disable()
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
localStorage.setItem('auto_launch', JSON.stringify(await isEnabled()))
|
||||
return isEnabled()
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export function getAutoLaunchStatusAsync(): boolean {
|
||||
return localStorage.getItem('auto_launch') === 'true'
|
||||
}
|
||||
@@ -9,16 +9,193 @@ 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'
|
||||
import { useToast, useConfirm } from 'primevue'
|
||||
import { loadMode, saveMode, type Mode } from '~/composables/mode'
|
||||
import ModeSwitcher from '~/components/ModeSwitcher.vue'
|
||||
import { getServiceStatus, type ServiceStatus } from '~/composables/backend'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const confirm = useConfirm()
|
||||
const aboutVisible = ref(false)
|
||||
const modeDialogVisible = ref(false)
|
||||
const currentMode = ref<Mode>({ mode: 'normal' })
|
||||
const editingMode = ref<Mode>({ mode: 'normal' })
|
||||
const isModeSaving = ref(false)
|
||||
const serviceStatus = ref<ServiceStatus>('NotInstalled')
|
||||
|
||||
async function openModeDialog() {
|
||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()))
|
||||
if (editingMode.value.mode === 'service') {
|
||||
serviceStatus.value = await getServiceStatus()
|
||||
}
|
||||
modeDialogVisible.value = true
|
||||
}
|
||||
|
||||
async function onModeSave() {
|
||||
if (isModeSaving.value) {
|
||||
return;
|
||||
}
|
||||
isModeSaving.value = true
|
||||
try {
|
||||
await initWithMode(editingMode.value);
|
||||
modeDialogVisible.value = false
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: e, life: 10000 })
|
||||
console.error("Error switching mode", e, currentMode.value, editingMode.value)
|
||||
await initWithMode(currentMode.value);
|
||||
}
|
||||
finally {
|
||||
isModeSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function onUninstallService() {
|
||||
confirm.require({
|
||||
message: t('mode.uninstall_service_confirm'),
|
||||
header: t('mode.uninstall_service'),
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectProps: {
|
||||
label: t('web.common.cancel'),
|
||||
severity: 'secondary',
|
||||
outlined: true
|
||||
},
|
||||
acceptProps: {
|
||||
label: t('mode.uninstall_service'),
|
||||
severity: 'danger'
|
||||
},
|
||||
accept: async () => {
|
||||
isModeSaving.value = true
|
||||
try {
|
||||
await initWithMode({ ...currentMode.value, mode: 'normal' });
|
||||
await initService(undefined)
|
||||
toast.add({ severity: 'success', summary: t('web.common.success'), detail: t('mode.uninstall_service_success'), life: 3000 })
|
||||
modeDialogVisible.value = false
|
||||
} catch (e: any) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: e, life: 10000 })
|
||||
console.error("Error uninstalling service", e)
|
||||
} finally {
|
||||
isModeSaving.value = false
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function onStopService() {
|
||||
isModeSaving.value = true
|
||||
try {
|
||||
await setServiceStatus(false)
|
||||
toast.add({ severity: 'success', summary: t('web.common.success'), detail: t('mode.stop_service_success'), life: 3000 })
|
||||
modeDialogVisible.value = false
|
||||
}
|
||||
catch (e: any) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: e, life: 10000 })
|
||||
console.error("Error stopping service", e)
|
||||
}
|
||||
finally {
|
||||
isModeSaving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function initWithMode(mode: Mode) {
|
||||
if (currentMode.value.mode === 'service' && mode.mode !== 'service') {
|
||||
let serviceStatus = await getServiceStatus()
|
||||
if (serviceStatus === "Running") {
|
||||
await setServiceStatus(false)
|
||||
serviceStatus = await getServiceStatus()
|
||||
}
|
||||
if (serviceStatus === "Stopped") {
|
||||
await initService(undefined)
|
||||
}
|
||||
}
|
||||
|
||||
let url: string | undefined = undefined
|
||||
let retrys = 1
|
||||
switch (mode.mode) {
|
||||
case 'remote':
|
||||
if (!mode.remote_rpc_address) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.remote_rpc_address_empty'), life: 10000 })
|
||||
return initWithMode({ ...mode, mode: 'normal' });
|
||||
}
|
||||
url = mode.remote_rpc_address
|
||||
break;
|
||||
case 'service':
|
||||
if (!mode.config_dir || !mode.file_log_dir || !mode.file_log_level || !mode.rpc_portal) {
|
||||
toast.add({ severity: 'error', summary: t('error'), detail: t('mode.service_config_empty'), life: 10000 })
|
||||
return initWithMode({ ...mode, mode: 'normal' });
|
||||
}
|
||||
let serviceStatus = await getServiceStatus()
|
||||
if (serviceStatus === "NotInstalled" || JSON.stringify(mode) !== JSON.stringify(currentMode.value)) {
|
||||
await initService({
|
||||
config_dir: mode.config_dir,
|
||||
file_log_dir: mode.file_log_dir,
|
||||
file_log_level: mode.file_log_level,
|
||||
rpc_portal: mode.rpc_portal,
|
||||
})
|
||||
serviceStatus = await getServiceStatus()
|
||||
}
|
||||
if (serviceStatus === "Stopped") {
|
||||
await setServiceStatus(true)
|
||||
}
|
||||
url = "tcp://" + mode.rpc_portal.replace("0.0.0.0", "127.0.0.1")
|
||||
retrys = 5
|
||||
break;
|
||||
}
|
||||
for (let i = 0; i < retrys; i++) {
|
||||
try {
|
||||
await connectRpcClient(url)
|
||||
break;
|
||||
} catch (e) {
|
||||
if (i === retrys - 1) {
|
||||
throw e;
|
||||
}
|
||||
console.error("Error connecting rpc client, retrying...", e)
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
}
|
||||
currentMode.value = mode
|
||||
saveMode(mode)
|
||||
clientRunning.value = await isClientRunning()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
currentMode.value = loadMode()
|
||||
initWithMode(currentMode.value);
|
||||
});
|
||||
|
||||
useTray(true)
|
||||
let toast = useToast();
|
||||
|
||||
const remoteClient = computed(() => new GUIRemoteClient());
|
||||
const instanceId = ref<string | undefined>(undefined);
|
||||
const clientRunning = ref(false);
|
||||
|
||||
watch(clientRunning, async (newVal, oldVal) => {
|
||||
if (!newVal && oldVal) {
|
||||
await reconnectClient()
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
clientRunning.value = await isClientRunning().catch(() => false)
|
||||
const timer = setInterval(async () => {
|
||||
try {
|
||||
clientRunning.value = await isClientRunning()
|
||||
} catch (e) {
|
||||
clientRunning.value = false
|
||||
console.error("Error checking client running status", e)
|
||||
}
|
||||
}, 1000)
|
||||
return () => {
|
||||
clearInterval(timer)
|
||||
}
|
||||
})
|
||||
async function reconnectClient() {
|
||||
editingMode.value = JSON.parse(JSON.stringify(loadMode()));
|
||||
await onModeSave()
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.setTimeout(async () => {
|
||||
@@ -81,11 +258,10 @@ const setting_menu_items: Ref<MenuItem[]> = ref([
|
||||
},
|
||||
},
|
||||
{
|
||||
label: () => getAutoLaunchStatus() ? t('disable_auto_launch') : t('enable_auto_launch'),
|
||||
icon: 'pi pi-desktop',
|
||||
command: async () => {
|
||||
await loadAutoLaunchStatusAsync(!getAutoLaunchStatus())
|
||||
},
|
||||
label: () => `${t('mode.switch_mode')}: ${t('mode.' + currentMode.value.mode)}`,
|
||||
icon: 'pi pi-sync',
|
||||
command: openModeDialog,
|
||||
visible: () => type() !== 'android',
|
||||
},
|
||||
{
|
||||
label: () => getDockVisibilityStatus() ? t('hide_dock_icon') : t('show_dock_icon'),
|
||||
@@ -117,6 +293,12 @@ const setting_menu_items: Ref<MenuItem[]> = ref([
|
||||
},
|
||||
])
|
||||
|
||||
async function connectRpcClient(url?: string) {
|
||||
await initRpcConnection(url)
|
||||
await sendConfigs()
|
||||
console.log("easytier rpc connection established")
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
if (type() === 'android') {
|
||||
try {
|
||||
@@ -127,7 +309,6 @@ onMounted(async () => {
|
||||
}
|
||||
}
|
||||
const unlisten = await listenGlobalEvents()
|
||||
await sendConfigs()
|
||||
return () => {
|
||||
unlisten()
|
||||
}
|
||||
@@ -140,9 +321,24 @@ onMounted(async () => {
|
||||
<Dialog v-model:visible="aboutVisible" modal :header="t('about.title')" :style="{ width: '70%' }">
|
||||
<About />
|
||||
</Dialog>
|
||||
<Dialog v-model:visible="modeDialogVisible" modal :header="t('mode.switch_mode')" :style="{ width: '50vw' }">
|
||||
<ModeSwitcher v-model="editingMode" @uninstall-service="onUninstallService" @stop-service="onStopService" />
|
||||
<template #footer>
|
||||
<Button :label="t('web.common.cancel')" icon="pi pi-times" @click="modeDialogVisible = false" text />
|
||||
<Button :label="t('web.common.save')" icon="pi pi-save" @click="onModeSave" autofocus :loading="isModeSaving" />
|
||||
</template>
|
||||
</Dialog>
|
||||
<Menu ref="log_menu" :model="log_menu_items_popup" :popup="true" />
|
||||
|
||||
<RemoteManagement class="flex-1 overflow-y-auto" :api="remoteClient" v-bind:instance-id="instanceId" />
|
||||
<RemoteManagement v-if="clientRunning" class="flex-1 overflow-y-auto" :api="remoteClient"
|
||||
:pause-auto-refresh="isModeSaving" v-bind:instance-id="instanceId" />
|
||||
<div v-else class="empty-state flex-1 flex flex-col items-center py-12">
|
||||
<i class="pi pi-server text-5xl text-secondary mb-4 opacity-50"></i>
|
||||
<div class="text-xl text-center font-medium mb-3">{{ t('client.not_running') }}
|
||||
</div>
|
||||
<Button @click="reconnectClient" :loading="isModeSaving" :label="t('client.retry')" icon="pi pi-replay"
|
||||
iconPos="left" />
|
||||
</div>
|
||||
|
||||
<Menubar :model="setting_menu_items" breakpoint="560px">
|
||||
<template #item="{ item, props }">
|
||||
|
||||
@@ -12,6 +12,7 @@ const { t } = useI18n()
|
||||
const props = defineProps<{
|
||||
api: Api.RemoteClient;
|
||||
newConfigGenerator?: () => NetworkTypes.NetworkConfig;
|
||||
pauseAutoRefresh?: boolean;
|
||||
}>();
|
||||
|
||||
const instanceId = defineModel('instanceId', {
|
||||
@@ -407,6 +408,9 @@ const actionMenu: Ref<MenuItem[]> = ref([
|
||||
]);
|
||||
|
||||
let periodFunc = new Utils.PeriodicTask(async () => {
|
||||
if (props.pauseAutoRefresh) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await Promise.all([loadNetworkInstanceIds(), loadCurrentNetworkInfo()]);
|
||||
} catch (e) {
|
||||
|
||||
@@ -340,3 +340,33 @@ web:
|
||||
language: 语言
|
||||
theme: 主题
|
||||
logout: 退出登录
|
||||
|
||||
mode:
|
||||
title: 模式
|
||||
switch_mode: 切换模式
|
||||
config_dir: 配置目录
|
||||
rpc_portal: RPC端口
|
||||
log_level: 日志级别
|
||||
log_dir: 日志目录
|
||||
remote_rpc_address: 远程RPC地址
|
||||
normal: 普通模式
|
||||
service: 服务模式
|
||||
remote: 远程模式
|
||||
normal_description: 直接运行EasyTier,适合本地使用
|
||||
service_description: 作为系统服务运行,支持开机自启和后台运行。退出GUI后服务仍在后台运行。
|
||||
remote_description: 连接到远程RPC服务,管理和控制远程节点
|
||||
service_status: 服务状态
|
||||
service_status_running: 运行中
|
||||
service_status_stopped: 已停止
|
||||
service_status_notinstalled: 未安装
|
||||
uninstall_service: 卸载服务
|
||||
stop_service: 停止服务
|
||||
uninstall_service_confirm: 确定要卸载服务吗?
|
||||
uninstall_service_success: 服务卸载成功,已切换回普通模式
|
||||
stop_service_success: 服务停止成功
|
||||
remote_rpc_address_empty: 远程RPC地址不能为空
|
||||
service_config_empty: 服务配置不能为空
|
||||
|
||||
client:
|
||||
not_running: 无法连接至远程客户端
|
||||
retry: 重试
|
||||
|
||||
@@ -340,3 +340,33 @@ web:
|
||||
language: Language
|
||||
theme: Theme
|
||||
logout: Logout
|
||||
|
||||
mode:
|
||||
title: Mode
|
||||
switch_mode: Switch Mode
|
||||
config_dir: Config Dir
|
||||
rpc_portal: RPC Portal
|
||||
log_level: Log Level
|
||||
log_dir: Log Dir
|
||||
remote_rpc_address: Remote RPC Address
|
||||
normal: Normal
|
||||
service: Service
|
||||
remote: Remote
|
||||
normal_description: Run EasyTier directly, suitable for local use.
|
||||
service_description: Run as a system service, supporting auto-startup and background operation. The service continues to run in the background after exiting the GUI.
|
||||
remote_description: Connect to a remote RPC service to manage and control remote nodes.
|
||||
service_status: Service Status
|
||||
service_status_running: Running
|
||||
service_status_stopped: Stopped
|
||||
service_status_notinstalled: Not Installed
|
||||
uninstall_service: Uninstall Service
|
||||
stop_service: Stop Service
|
||||
uninstall_service_confirm: Are you sure you want to uninstall the service?
|
||||
uninstall_service_success: Service uninstalled successfully, switched back to normal mode
|
||||
stop_service_success: Service stopped successfully
|
||||
remote_rpc_address_empty: Remote RPC Address cannot be empty
|
||||
service_config_empty: Service Config cannot be empty
|
||||
|
||||
client:
|
||||
not_running: Unable to connect to remote client.
|
||||
retry: Retry
|
||||
|
||||
+1
-1
@@ -189,7 +189,7 @@ sys-locale = "0.3"
|
||||
ringbuf = "0.4.5"
|
||||
async-ringbuf = "0.3.1"
|
||||
|
||||
service-manager = { git = "https://github.com/chipsenkbeil/service-manager-rs.git", branch = "main" }
|
||||
service-manager = { git = "https://github.com/EasyTier/service-manager-rs.git", branch = "main" }
|
||||
|
||||
zstd = { version = "0.13" }
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
ffi::OsString,
|
||||
fmt::Write,
|
||||
net::{IpAddr, SocketAddr},
|
||||
path::PathBuf,
|
||||
str::FromStr,
|
||||
@@ -17,6 +16,8 @@ use humansize::format_size;
|
||||
use rust_i18n::t;
|
||||
use service_manager::*;
|
||||
use tabled::settings::Style;
|
||||
|
||||
use easytier::service_manager::{Service, ServiceInstallOptions};
|
||||
use tokio::time::timeout;
|
||||
|
||||
use easytier::{
|
||||
@@ -1376,274 +1377,6 @@ impl CommandHandler<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServiceInstallOptions {
|
||||
pub program: PathBuf,
|
||||
pub args: Vec<OsString>,
|
||||
pub work_directory: PathBuf,
|
||||
pub disable_autostart: bool,
|
||||
pub description: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub disable_restart_on_failure: bool,
|
||||
}
|
||||
pub struct Service {
|
||||
lable: ServiceLabel,
|
||||
kind: ServiceManagerKind,
|
||||
service_manager: Box<dyn ServiceManager>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn new(name: String) -> Result<Self, Error> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let service_manager = Box::new(crate::win_service_manager::WinServiceManager::new()?);
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
let service_manager = <dyn ServiceManager>::native()?;
|
||||
let kind = ServiceManagerKind::native()?;
|
||||
|
||||
println!("service manager kind: {:?}", kind);
|
||||
|
||||
Ok(Self {
|
||||
lable: name.parse()?,
|
||||
kind,
|
||||
service_manager,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn install(&self, options: &ServiceInstallOptions) -> Result<(), Error> {
|
||||
let ctx = ServiceInstallCtx {
|
||||
label: self.lable.clone(),
|
||||
program: options.program.clone(),
|
||||
args: options.args.clone(),
|
||||
contents: self.make_install_content_option(options),
|
||||
autostart: !options.disable_autostart,
|
||||
username: None,
|
||||
working_directory: Some(options.work_directory.clone()),
|
||||
environment: None,
|
||||
disable_restart_on_failure: options.disable_restart_on_failure,
|
||||
};
|
||||
if self.status()? != ServiceStatus::NotInstalled {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Service is already installed! Service Name: {}",
|
||||
self.lable
|
||||
));
|
||||
}
|
||||
|
||||
self.service_manager
|
||||
.install(ctx.clone())
|
||||
.map_err(|e| anyhow::anyhow!("failed to install service: {:?}", e))?;
|
||||
|
||||
println!(
|
||||
"Service installed successfully! Service Name: {}",
|
||||
self.lable
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uninstall(&self) -> Result<(), Error> {
|
||||
let ctx = ServiceUninstallCtx {
|
||||
label: self.lable.clone(),
|
||||
};
|
||||
let status = self.status()?;
|
||||
|
||||
if status == ServiceStatus::NotInstalled {
|
||||
return Err(anyhow::anyhow!("Service is not installed"));
|
||||
}
|
||||
|
||||
if status == ServiceStatus::Running {
|
||||
self.service_manager.stop(ServiceStopCtx {
|
||||
label: self.lable.clone(),
|
||||
})?;
|
||||
}
|
||||
|
||||
self.service_manager
|
||||
.uninstall(ctx)
|
||||
.map_err(|e| anyhow::anyhow!("failed to uninstall service: {}", e))
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Result<ServiceStatus, Error> {
|
||||
let ctx = ServiceStatusCtx {
|
||||
label: self.lable.clone(),
|
||||
};
|
||||
let status = self.service_manager.status(ctx)?;
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Result<(), Error> {
|
||||
let ctx = ServiceStartCtx {
|
||||
label: self.lable.clone(),
|
||||
};
|
||||
let status = self.status()?;
|
||||
|
||||
match status {
|
||||
ServiceStatus::Running => Err(anyhow::anyhow!("Service is already running")),
|
||||
ServiceStatus::Stopped(_) => {
|
||||
self.service_manager
|
||||
.start(ctx)
|
||||
.map_err(|e| anyhow::anyhow!("failed to start service: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
ServiceStatus::NotInstalled => Err(anyhow::anyhow!("Service is not installed")),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<(), Error> {
|
||||
let ctx = ServiceStopCtx {
|
||||
label: self.lable.clone(),
|
||||
};
|
||||
let status = self.status()?;
|
||||
|
||||
match status {
|
||||
ServiceStatus::Running => {
|
||||
self.service_manager
|
||||
.stop(ctx)
|
||||
.map_err(|e| anyhow::anyhow!("failed to stop service: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
ServiceStatus::Stopped(_) => Err(anyhow::anyhow!("Service is already stopped")),
|
||||
ServiceStatus::NotInstalled => Err(anyhow::anyhow!("Service is not installed")),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_install_content_option(&self, options: &ServiceInstallOptions) -> Option<String> {
|
||||
match self.kind {
|
||||
ServiceManagerKind::Systemd => Some(self.make_systemd_unit(options).unwrap()),
|
||||
ServiceManagerKind::Rcd => Some(self.make_rcd_script(options).unwrap()),
|
||||
ServiceManagerKind::OpenRc => Some(self.make_open_rc_script(options).unwrap()),
|
||||
ServiceManagerKind::Launchd => None, // 使用 service-manager-rs 的默认 plist 生成
|
||||
_ => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let win_options = win_service_manager::WinServiceInstallOptions {
|
||||
description: options.description.clone(),
|
||||
display_name: options.display_name.clone(),
|
||||
dependencies: Some(vec!["rpcss".to_string(), "dnscache".to_string()]),
|
||||
};
|
||||
|
||||
Some(serde_json::to_string(&win_options).unwrap())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_systemd_unit(
|
||||
&self,
|
||||
options: &ServiceInstallOptions,
|
||||
) -> Result<String, std::fmt::Error> {
|
||||
let args = options
|
||||
.args
|
||||
.iter()
|
||||
.map(|a| a.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let target_app = options.program.display().to_string();
|
||||
let work_dir = options.work_directory.display().to_string();
|
||||
let mut unit_content = String::new();
|
||||
|
||||
writeln!(unit_content, "[Unit]")?;
|
||||
writeln!(unit_content, "After=network.target syslog.target")?;
|
||||
if let Some(ref d) = options.description {
|
||||
writeln!(unit_content, "Description={d}")?;
|
||||
}
|
||||
writeln!(unit_content, "StartLimitIntervalSec=0")?;
|
||||
writeln!(unit_content)?;
|
||||
writeln!(unit_content, "[Service]")?;
|
||||
writeln!(unit_content, "Type=simple")?;
|
||||
writeln!(unit_content, "WorkingDirectory={work_dir}")?;
|
||||
writeln!(unit_content, "ExecStart={target_app} {args}")?;
|
||||
writeln!(unit_content, "Restart=always")?;
|
||||
writeln!(unit_content, "RestartSec=1")?;
|
||||
writeln!(unit_content, "LimitNOFILE=infinity")?;
|
||||
writeln!(unit_content)?;
|
||||
writeln!(unit_content, "[Install]")?;
|
||||
writeln!(unit_content, "WantedBy=multi-user.target")?;
|
||||
|
||||
std::result::Result::Ok(unit_content)
|
||||
}
|
||||
|
||||
fn make_rcd_script(&self, options: &ServiceInstallOptions) -> Result<String, std::fmt::Error> {
|
||||
let name = self.lable.to_qualified_name();
|
||||
let args = options
|
||||
.args
|
||||
.iter()
|
||||
.map(|a| a.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let target_app = options.program.display().to_string();
|
||||
let work_dir = options.work_directory.display().to_string();
|
||||
let mut script = String::new();
|
||||
|
||||
writeln!(script, "#!/bin/sh")?;
|
||||
writeln!(script, "#")?;
|
||||
writeln!(script, "# PROVIDE: {name}")?;
|
||||
writeln!(script, "# REQUIRE: LOGIN FILESYSTEMS NETWORKING ")?;
|
||||
writeln!(script, "# KEYWORD: shutdown")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, ". /etc/rc.subr")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, "name=\"{name}\"")?;
|
||||
if let Some(ref d) = options.description {
|
||||
writeln!(script, "desc=\"{d}\"")?;
|
||||
}
|
||||
writeln!(script, "rcvar=\"{name}_enable\"")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, "load_rc_config ${{name}}")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, ": ${{{name}_options=\"{args}\"}}")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, "{name}_chdir=\"{work_dir}\"")?;
|
||||
writeln!(script, "pidfile=\"/var/run/${{name}}.pid\"")?;
|
||||
writeln!(script, "procname=\"{target_app}\"")?;
|
||||
writeln!(script, "command=\"/usr/sbin/daemon\"")?;
|
||||
writeln!(
|
||||
script,
|
||||
"command_args=\"-c -S -T ${{name}} -p ${{pidfile}} ${{procname}} ${{{name}_options}}\""
|
||||
)?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, "run_rc_command \"$1\"")?;
|
||||
|
||||
std::result::Result::Ok(script)
|
||||
}
|
||||
|
||||
fn make_open_rc_script(
|
||||
&self,
|
||||
options: &ServiceInstallOptions,
|
||||
) -> Result<String, std::fmt::Error> {
|
||||
let args = options
|
||||
.args
|
||||
.iter()
|
||||
.map(|a| a.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let target_app = options.program.display().to_string();
|
||||
let work_dir = options.work_directory.display().to_string();
|
||||
let mut script = String::new();
|
||||
|
||||
writeln!(script, "#!/sbin/openrc-run")?;
|
||||
writeln!(script)?;
|
||||
if let Some(ref d) = options.description {
|
||||
writeln!(script, "description=\"{d}\"")?;
|
||||
}
|
||||
writeln!(script, "command=\"{target_app}\"")?;
|
||||
writeln!(script, "command_args=\"{args}\"")?;
|
||||
writeln!(script, "pidfile=\"/run/${{RC_SVCNAME}}.pid\"")?;
|
||||
writeln!(script, "command_background=\"yes\"")?;
|
||||
writeln!(script, "directory=\"{work_dir}\"")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, "depend() {{")?;
|
||||
writeln!(script, " need net")?;
|
||||
writeln!(script, " use looger")?;
|
||||
writeln!(script, "}}")?;
|
||||
|
||||
std::result::Result::Ok(script)
|
||||
}
|
||||
}
|
||||
|
||||
fn print_output<T>(items: &[T], format: &OutputFormat) -> Result<(), Error>
|
||||
where
|
||||
T: tabled::Tabled + serde::Serialize,
|
||||
@@ -2225,180 +1958,3 @@ async fn main() -> Result<(), Error> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win_service_manager {
|
||||
use std::{ffi::OsStr, ffi::OsString, io, path::PathBuf};
|
||||
use windows_service::{
|
||||
service::{
|
||||
ServiceAccess, ServiceDependency, ServiceErrorControl, ServiceInfo, ServiceStartType,
|
||||
ServiceType,
|
||||
},
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
|
||||
use service_manager::{
|
||||
ServiceInstallCtx, ServiceLevel, ServiceStartCtx, ServiceStatus, ServiceStatusCtx,
|
||||
ServiceStopCtx, ServiceUninstallCtx,
|
||||
};
|
||||
|
||||
use winreg::{enums::*, RegKey};
|
||||
|
||||
use easytier::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WinServiceInstallOptions {
|
||||
pub dependencies: Option<Vec<String>>,
|
||||
pub description: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
pub struct WinServiceManager {
|
||||
service_manager: ServiceManager,
|
||||
}
|
||||
|
||||
impl WinServiceManager {
|
||||
pub fn new() -> Result<Self, crate::Error> {
|
||||
let service_manager =
|
||||
ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::ALL_ACCESS)?;
|
||||
Ok(Self { service_manager })
|
||||
}
|
||||
}
|
||||
impl service_manager::ServiceManager for WinServiceManager {
|
||||
fn available(&self) -> io::Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
|
||||
let start_type_ = if ctx.autostart {
|
||||
ServiceStartType::AutoStart
|
||||
} else {
|
||||
ServiceStartType::OnDemand
|
||||
};
|
||||
let srv_name = OsString::from(ctx.label.to_qualified_name());
|
||||
let mut dis_name = srv_name.clone();
|
||||
let mut description: Option<OsString> = None;
|
||||
let mut dependencies = Vec::<ServiceDependency>::new();
|
||||
|
||||
if let Some(s) = ctx.contents.as_ref() {
|
||||
let options: WinServiceInstallOptions = serde_json::from_str(s.as_str()).unwrap();
|
||||
if let Some(d) = options.dependencies {
|
||||
dependencies = d
|
||||
.iter()
|
||||
.map(|dep| ServiceDependency::Service(OsString::from(dep.clone())))
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
if let Some(d) = options.description {
|
||||
description = Some(OsString::from(d));
|
||||
}
|
||||
if let Some(d) = options.display_name {
|
||||
dis_name = OsString::from(d);
|
||||
}
|
||||
}
|
||||
|
||||
let service_info = ServiceInfo {
|
||||
name: srv_name,
|
||||
display_name: dis_name,
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
start_type: start_type_,
|
||||
error_control: ServiceErrorControl::Normal,
|
||||
executable_path: ctx.program,
|
||||
launch_arguments: ctx.args,
|
||||
dependencies: dependencies.clone(),
|
||||
account_name: None,
|
||||
account_password: None,
|
||||
};
|
||||
|
||||
let service = self
|
||||
.service_manager
|
||||
.create_service(&service_info, ServiceAccess::ALL_ACCESS)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
if let Some(s) = description {
|
||||
service
|
||||
.set_description(s.clone())
|
||||
.map_err(io::Error::other)?;
|
||||
}
|
||||
|
||||
if let Some(work_dir) = ctx.working_directory {
|
||||
set_service_work_directory(&ctx.label.to_qualified_name(), work_dir)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
|
||||
let service = self
|
||||
.service_manager
|
||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
service.delete().map_err(io::Error::other)
|
||||
}
|
||||
|
||||
fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
|
||||
let service = self
|
||||
.service_manager
|
||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
service.start(&[] as &[&OsStr]).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
|
||||
let service = self
|
||||
.service_manager
|
||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
_ = service.stop().map_err(io::Error::other)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn level(&self) -> ServiceLevel {
|
||||
ServiceLevel::System
|
||||
}
|
||||
|
||||
fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
|
||||
match level {
|
||||
ServiceLevel::System => Ok(()),
|
||||
_ => Err(io::Error::other("Unsupported service level")),
|
||||
}
|
||||
}
|
||||
|
||||
fn status(&self, ctx: ServiceStatusCtx) -> io::Result<ServiceStatus> {
|
||||
let service = match self
|
||||
.service_manager
|
||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::QUERY_STATUS)
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
if let windows_service::Error::Winapi(ref win_err) = e {
|
||||
if win_err.raw_os_error() == Some(0x424) {
|
||||
return Ok(ServiceStatus::NotInstalled);
|
||||
}
|
||||
}
|
||||
return Err(io::Error::other(e));
|
||||
}
|
||||
};
|
||||
|
||||
let status = service.query_status().map_err(io::Error::other)?;
|
||||
|
||||
match status.current_state {
|
||||
windows_service::service::ServiceState::Stopped => Ok(ServiceStatus::Stopped(None)),
|
||||
_ => Ok(ServiceStatus::Running),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_service_work_directory(service_name: &str, work_directory: PathBuf) -> io::Result<()> {
|
||||
let (reg_key, _) =
|
||||
RegKey::predef(HKEY_LOCAL_MACHINE).create_subkey(WIN_SERVICE_WORK_DIR_REG_KEY)?;
|
||||
reg_key
|
||||
.set_value::<OsString, _>(service_name, &work_directory.as_os_str().to_os_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1385
File diff suppressed because it is too large
Load Diff
@@ -13,11 +13,11 @@ use crate::{
|
||||
rpc_service::InstanceRpcService,
|
||||
};
|
||||
|
||||
pub(crate) struct WebClientGuard {
|
||||
pub(crate) struct DaemonGuard {
|
||||
guard: Option<Arc<()>>,
|
||||
stop_check_notifier: Arc<tokio::sync::Notify>,
|
||||
}
|
||||
impl Drop for WebClientGuard {
|
||||
impl Drop for DaemonGuard {
|
||||
fn drop(&mut self) {
|
||||
drop(self.guard.take());
|
||||
self.stop_check_notifier.notify_one();
|
||||
@@ -30,7 +30,7 @@ pub struct NetworkInstanceManager {
|
||||
stop_check_notifier: Arc<tokio::sync::Notify>,
|
||||
instance_error_messages: Arc<DashMap<uuid::Uuid, String>>,
|
||||
config_dir: Option<PathBuf>,
|
||||
web_client_counter: Arc<()>,
|
||||
guard_counter: Arc<()>,
|
||||
}
|
||||
|
||||
impl Default for NetworkInstanceManager {
|
||||
@@ -47,7 +47,7 @@ impl NetworkInstanceManager {
|
||||
stop_check_notifier: Arc::new(tokio::sync::Notify::new()),
|
||||
instance_error_messages: Arc::new(DashMap::new()),
|
||||
config_dir: None,
|
||||
web_client_counter: Arc::new(()),
|
||||
guard_counter: Arc::new(()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,9 +233,9 @@ impl NetworkInstanceManager {
|
||||
self.config_dir.as_ref()
|
||||
}
|
||||
|
||||
pub(crate) fn register_web_client(&self) -> WebClientGuard {
|
||||
WebClientGuard {
|
||||
guard: Some(self.web_client_counter.clone()),
|
||||
pub(crate) fn register_daemon(&self) -> DaemonGuard {
|
||||
DaemonGuard {
|
||||
guard: Some(self.guard_counter.clone()),
|
||||
stop_check_notifier: self.stop_check_notifier.clone(),
|
||||
}
|
||||
}
|
||||
@@ -250,9 +250,9 @@ impl NetworkInstanceManager {
|
||||
.instance_map
|
||||
.iter()
|
||||
.any(|item| item.value().is_easytier_running());
|
||||
let web_client_running = Arc::strong_count(&self.web_client_counter) > 1;
|
||||
let daemon_running = Arc::strong_count(&self.guard_counter) > 1;
|
||||
|
||||
if !local_instance_running && !web_client_running {
|
||||
if !local_instance_running && !daemon_running {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,11 +13,13 @@ mod vpn_portal;
|
||||
|
||||
pub mod common;
|
||||
pub mod connector;
|
||||
pub mod core;
|
||||
pub mod instance_manager;
|
||||
pub mod launcher;
|
||||
pub mod peers;
|
||||
pub mod proto;
|
||||
pub mod rpc_service;
|
||||
pub mod service_manager;
|
||||
pub mod tunnel;
|
||||
pub mod utils;
|
||||
pub mod web_client;
|
||||
|
||||
@@ -39,6 +39,7 @@ pub struct StandAloneServer<L> {
|
||||
inflight_server: Arc<AtomicU32>,
|
||||
tasks: JoinSet<()>,
|
||||
hook: Option<Arc<dyn RpcServerHook>>,
|
||||
rx_timeout: Option<Duration>,
|
||||
}
|
||||
|
||||
impl<L: TunnelListener + 'static> StandAloneServer<L> {
|
||||
@@ -50,9 +51,14 @@ impl<L: TunnelListener + 'static> StandAloneServer<L> {
|
||||
tasks: JoinSet::new(),
|
||||
|
||||
hook: None,
|
||||
rx_timeout: Some(Duration::from_secs(60)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_rx_timeout(&mut self, timeout: Option<Duration>) {
|
||||
self.rx_timeout = timeout;
|
||||
}
|
||||
|
||||
pub fn set_hook(&mut self, hook: Arc<dyn RpcServerHook>) {
|
||||
self.hook = Some(hook);
|
||||
}
|
||||
@@ -66,6 +72,7 @@ impl<L: TunnelListener + 'static> StandAloneServer<L> {
|
||||
inflight: Arc<AtomicU32>,
|
||||
registry: Arc<ServiceRegistry>,
|
||||
hook: Arc<dyn RpcServerHook>,
|
||||
rx_timeout: Option<Duration>,
|
||||
) -> Result<(), Error> {
|
||||
let tasks = Arc::new(Mutex::new(JoinSet::new()));
|
||||
join_joinset_background(tasks.clone(), "standalone serve_loop".to_string());
|
||||
@@ -87,8 +94,7 @@ impl<L: TunnelListener + 'static> StandAloneServer<L> {
|
||||
|
||||
inflight_server.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||
tasks.lock().unwrap().spawn(async move {
|
||||
let server =
|
||||
BidirectRpcManager::new().set_rx_timeout(Some(Duration::from_secs(60)));
|
||||
let server = BidirectRpcManager::new().set_rx_timeout(rx_timeout);
|
||||
server.rpc_server().registry().replace_registry(®istry);
|
||||
server.run_with_tunnel(tunnel);
|
||||
server.wait().await;
|
||||
@@ -101,6 +107,7 @@ impl<L: TunnelListener + 'static> StandAloneServer<L> {
|
||||
pub async fn serve(&mut self) -> Result<(), Error> {
|
||||
let mut listener = self.listener.take().unwrap();
|
||||
let hook = self.hook.take().unwrap_or_else(|| Arc::new(DefaultHook));
|
||||
let rx_timeout = self.rx_timeout;
|
||||
|
||||
listener
|
||||
.listen()
|
||||
@@ -118,6 +125,7 @@ impl<L: TunnelListener + 'static> StandAloneServer<L> {
|
||||
inflight_server.clone(),
|
||||
registry.clone(),
|
||||
hook.clone(),
|
||||
rx_timeout,
|
||||
)
|
||||
.await;
|
||||
if let Err(e) = ret {
|
||||
|
||||
@@ -70,6 +70,11 @@ impl<T: TunnelListener + 'static> ApiRpcServer<T> {
|
||||
self.rpc_server.serve().await?;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn with_rx_timeout(mut self, timeout: Option<std::time::Duration>) -> Self {
|
||||
self.rpc_server.set_rx_timeout(timeout);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: TunnelListener + 'static> Drop for ApiRpcServer<T> {
|
||||
|
||||
@@ -26,7 +26,7 @@ impl LoggerRpcService {
|
||||
}
|
||||
}
|
||||
|
||||
fn string_to_log_level(level_str: &str) -> LogLevel {
|
||||
pub fn string_to_log_level(level_str: &str) -> LogLevel {
|
||||
match level_str.to_lowercase().as_str() {
|
||||
"off" | "disabled" => LogLevel::Disabled,
|
||||
"error" => LogLevel::Error,
|
||||
|
||||
@@ -0,0 +1,541 @@
|
||||
use std::ffi::OsString;
|
||||
use std::fmt::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use service_manager::ServiceManager as _;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ServiceInstallOptions {
|
||||
pub program: PathBuf,
|
||||
pub args: Vec<OsString>,
|
||||
pub work_directory: PathBuf,
|
||||
pub disable_autostart: bool,
|
||||
pub description: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub disable_restart_on_failure: bool,
|
||||
}
|
||||
|
||||
pub type ServiceStatus = service_manager::ServiceStatus;
|
||||
|
||||
trait ServiceManager: service_manager::ServiceManager {
|
||||
fn update(&self, ctx: service_manager::ServiceInstallCtx) -> std::io::Result<()>;
|
||||
}
|
||||
impl ServiceManager for service_manager::TypedServiceManager {
|
||||
fn update(&self, ctx: service_manager::ServiceInstallCtx) -> std::io::Result<()> {
|
||||
let status = self.status(service_manager::ServiceStatusCtx {
|
||||
label: ctx.label.clone(),
|
||||
})?;
|
||||
if status == ServiceStatus::Running {
|
||||
self.stop(service_manager::ServiceStopCtx {
|
||||
label: ctx.label.clone(),
|
||||
})?;
|
||||
}
|
||||
if status != ServiceStatus::NotInstalled {
|
||||
self.uninstall(service_manager::ServiceUninstallCtx {
|
||||
label: ctx.label.clone(),
|
||||
})?;
|
||||
}
|
||||
self.install(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Service {
|
||||
label: service_manager::ServiceLabel,
|
||||
kind: service_manager::ServiceManagerKind,
|
||||
service_manager: Box<dyn ServiceManager>,
|
||||
}
|
||||
|
||||
impl Service {
|
||||
pub fn new(name: String) -> Result<Self, anyhow::Error> {
|
||||
#[cfg(target_os = "windows")]
|
||||
let service_manager = Box::new(self::win_service_manager::WinServiceManager::new()?);
|
||||
#[cfg(target_os = "macos")]
|
||||
let service_manager: Box<dyn ServiceManager> =
|
||||
Box::new(service_manager::TypedServiceManager::Launchd(
|
||||
service_manager::LaunchdServiceManager::system().with_config(
|
||||
service_manager::LaunchdConfig {
|
||||
install: service_manager::LaunchdInstallConfig {
|
||||
keep_alive: service_manager::KeepAlive::conditions()
|
||||
.crashed(true)
|
||||
.successful_exit(false),
|
||||
},
|
||||
},
|
||||
),
|
||||
));
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
let service_manager: Box<dyn ServiceManager> =
|
||||
Box::new(service_manager::TypedServiceManager::native()?);
|
||||
|
||||
let kind = service_manager::ServiceManagerKind::native()?;
|
||||
|
||||
println!("service manager kind: {:?}", kind);
|
||||
|
||||
Ok(Self {
|
||||
label: name.parse()?,
|
||||
kind,
|
||||
service_manager,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn install(&self, options: &ServiceInstallOptions) -> Result<(), anyhow::Error> {
|
||||
let ctx = service_manager::ServiceInstallCtx {
|
||||
label: self.label.clone(),
|
||||
program: options.program.clone(),
|
||||
args: options.args.clone(),
|
||||
contents: self.make_install_content_option(options),
|
||||
autostart: !options.disable_autostart,
|
||||
username: None,
|
||||
working_directory: Some(options.work_directory.clone()),
|
||||
environment: None,
|
||||
disable_restart_on_failure: options.disable_restart_on_failure,
|
||||
};
|
||||
|
||||
if self.status()? != service_manager::ServiceStatus::NotInstalled {
|
||||
self.service_manager
|
||||
.update(ctx)
|
||||
.map_err(|e| anyhow::anyhow!("failed to update service: {:?}", e))?;
|
||||
println!("Service updated successfully! Service Name: {}", self.label);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.service_manager
|
||||
.install(ctx.clone())
|
||||
.map_err(|e| anyhow::anyhow!("failed to install service: {:?}", e))?;
|
||||
|
||||
println!(
|
||||
"Service installed successfully! Service Name: {}",
|
||||
self.label
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn uninstall(&self) -> Result<(), anyhow::Error> {
|
||||
let ctx = service_manager::ServiceUninstallCtx {
|
||||
label: self.label.clone(),
|
||||
};
|
||||
let status = self.status()?;
|
||||
|
||||
if status == service_manager::ServiceStatus::NotInstalled {
|
||||
return Err(anyhow::anyhow!("Service is not installed"))?;
|
||||
}
|
||||
|
||||
if status == service_manager::ServiceStatus::Running {
|
||||
self.service_manager.stop(service_manager::ServiceStopCtx {
|
||||
label: self.label.clone(),
|
||||
})?;
|
||||
}
|
||||
|
||||
self.service_manager
|
||||
.uninstall(ctx)
|
||||
.map_err(|e| anyhow::anyhow!("failed to uninstall service: {}", e))
|
||||
}
|
||||
|
||||
pub fn status(&self) -> Result<service_manager::ServiceStatus, anyhow::Error> {
|
||||
let ctx = service_manager::ServiceStatusCtx {
|
||||
label: self.label.clone(),
|
||||
};
|
||||
let status = self.service_manager.status(ctx)?;
|
||||
|
||||
Ok(status)
|
||||
}
|
||||
|
||||
pub fn start(&self) -> Result<(), anyhow::Error> {
|
||||
let ctx = service_manager::ServiceStartCtx {
|
||||
label: self.label.clone(),
|
||||
};
|
||||
let status = self.status()?;
|
||||
|
||||
match status {
|
||||
service_manager::ServiceStatus::Running => {
|
||||
Err(anyhow::anyhow!("Service is already running"))?
|
||||
}
|
||||
service_manager::ServiceStatus::Stopped(_) => {
|
||||
self.service_manager
|
||||
.start(ctx)
|
||||
.map_err(|e| anyhow::anyhow!("failed to start service: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
service_manager::ServiceStatus::NotInstalled => {
|
||||
Err(anyhow::anyhow!("Service is not installed"))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&self) -> Result<(), anyhow::Error> {
|
||||
let ctx = service_manager::ServiceStopCtx {
|
||||
label: self.label.clone(),
|
||||
};
|
||||
let status = self.status()?;
|
||||
|
||||
match status {
|
||||
service_manager::ServiceStatus::Running => {
|
||||
self.service_manager
|
||||
.stop(ctx)
|
||||
.map_err(|e| anyhow::anyhow!("failed to stop service: {}", e))?;
|
||||
Ok(())
|
||||
}
|
||||
service_manager::ServiceStatus::Stopped(_) => {
|
||||
Err(anyhow::anyhow!("Service is already stopped"))?
|
||||
}
|
||||
service_manager::ServiceStatus::NotInstalled => {
|
||||
Err(anyhow::anyhow!("Service is not installed"))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_install_content_option(&self, options: &ServiceInstallOptions) -> Option<String> {
|
||||
match self.kind {
|
||||
service_manager::ServiceManagerKind::Systemd => {
|
||||
Some(self.make_systemd_unit(options).unwrap())
|
||||
}
|
||||
service_manager::ServiceManagerKind::Rcd => {
|
||||
Some(self.make_rcd_script(options).unwrap())
|
||||
}
|
||||
service_manager::ServiceManagerKind::OpenRc => {
|
||||
Some(self.make_open_rc_script(options).unwrap())
|
||||
}
|
||||
service_manager::ServiceManagerKind::Launchd => {
|
||||
None // 使用 service-manager-rs 的默认 plist 生成
|
||||
}
|
||||
_ => {
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
let win_options = self::win_service_manager::WinServiceInstallOptions {
|
||||
description: options.description.clone(),
|
||||
display_name: options.display_name.clone(),
|
||||
dependencies: Some(vec!["rpcss".to_string(), "dnscache".to_string()]),
|
||||
};
|
||||
|
||||
Some(serde_json::to_string(&win_options).unwrap())
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_systemd_unit(
|
||||
&self,
|
||||
options: &ServiceInstallOptions,
|
||||
) -> Result<String, std::fmt::Error> {
|
||||
let args = options
|
||||
.args
|
||||
.iter()
|
||||
.map(|a| a.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let target_app = options.program.display().to_string();
|
||||
let work_dir = options.work_directory.display().to_string();
|
||||
let mut unit_content = String::new();
|
||||
|
||||
writeln!(unit_content, "[Unit]")?;
|
||||
writeln!(unit_content, "After=network.target syslog.target")?;
|
||||
if let Some(ref d) = options.description {
|
||||
writeln!(unit_content, "Description={d}")?;
|
||||
}
|
||||
writeln!(unit_content, "StartLimitIntervalSec=0")?;
|
||||
writeln!(unit_content)?;
|
||||
writeln!(unit_content, "[Service]")?;
|
||||
writeln!(unit_content, "Type=simple")?;
|
||||
writeln!(unit_content, "WorkingDirectory={work_dir}")?;
|
||||
writeln!(unit_content, "ExecStart={target_app} {args}")?;
|
||||
writeln!(unit_content, "Restart=always")?;
|
||||
writeln!(unit_content, "RestartSec=1")?;
|
||||
writeln!(unit_content, "LimitNOFILE=infinity")?;
|
||||
writeln!(unit_content)?;
|
||||
writeln!(unit_content, "[Install]")?;
|
||||
writeln!(unit_content, "WantedBy=multi-user.target")?;
|
||||
|
||||
std::result::Result::Ok(unit_content)
|
||||
}
|
||||
|
||||
fn make_rcd_script(&self, options: &ServiceInstallOptions) -> Result<String, std::fmt::Error> {
|
||||
let name = self.label.to_qualified_name();
|
||||
let args = options
|
||||
.args
|
||||
.iter()
|
||||
.map(|a| a.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let target_app = options.program.display().to_string();
|
||||
let work_dir = options.work_directory.display().to_string();
|
||||
let mut script = String::new();
|
||||
|
||||
writeln!(script, "#!/bin/sh")?;
|
||||
writeln!(script, "#")?;
|
||||
writeln!(script, "# PROVIDE: {name}")?;
|
||||
writeln!(script, "# REQUIRE: LOGIN FILESYSTEMS NETWORKING ")?;
|
||||
writeln!(script, "# KEYWORD: shutdown")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, ". /etc/rc.subr")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, "name=\"{name}\"")?;
|
||||
if let Some(ref d) = options.description {
|
||||
writeln!(script, "desc=\"{d}\"")?;
|
||||
}
|
||||
writeln!(script, "rcvar=\"{name}_enable\"")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, "load_rc_config ${{name}}")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, ": ${{{name}_options=\"{args}\"}}")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, "{name}_chdir=\"{work_dir}\"")?;
|
||||
writeln!(script, "pidfile=\"/var/run/${{name}}.pid\"")?;
|
||||
writeln!(script, "procname=\"{target_app}\"")?;
|
||||
writeln!(script, "command=\"/usr/sbin/daemon\"")?;
|
||||
writeln!(
|
||||
script,
|
||||
"command_args=\"-c -S -T ${{name}} -p ${{pidfile}} ${{procname}} ${{{name}_options}}\""
|
||||
)?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, "run_rc_command \"$1\"")?;
|
||||
|
||||
std::result::Result::Ok(script)
|
||||
}
|
||||
|
||||
fn make_open_rc_script(
|
||||
&self,
|
||||
options: &ServiceInstallOptions,
|
||||
) -> Result<String, std::fmt::Error> {
|
||||
let args = options
|
||||
.args
|
||||
.iter()
|
||||
.map(|a| a.to_string_lossy())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
let target_app = options.program.display().to_string();
|
||||
let work_dir = options.work_directory.display().to_string();
|
||||
let mut script = String::new();
|
||||
|
||||
writeln!(script, "#!/sbin/openrc-run")?;
|
||||
writeln!(script)?;
|
||||
if let Some(ref d) = options.description {
|
||||
writeln!(script, "description=\"{d}\"")?;
|
||||
}
|
||||
writeln!(script, "command=\"{target_app}\"")?;
|
||||
writeln!(script, "command_args=\"{args}\"")?;
|
||||
writeln!(script, "pidfile=\"/run/${{RC_SVCNAME}}.pid\"")?;
|
||||
writeln!(script, "command_background=\"yes\"")?;
|
||||
writeln!(script, "directory=\"{work_dir}\"")?;
|
||||
writeln!(script)?;
|
||||
writeln!(script, "depend() {{")?;
|
||||
writeln!(script, " need net")?;
|
||||
writeln!(script, " use looger")?;
|
||||
writeln!(script, "}}")?;
|
||||
|
||||
std::result::Result::Ok(script)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win_service_manager {
|
||||
use std::{ffi::OsStr, ffi::OsString, io, path::PathBuf};
|
||||
use windows_service::{
|
||||
service::{
|
||||
ServiceAccess, ServiceDependency, ServiceErrorControl, ServiceInfo, ServiceStartType,
|
||||
ServiceType,
|
||||
},
|
||||
service_manager::{ServiceManager, ServiceManagerAccess},
|
||||
};
|
||||
|
||||
use service_manager::{
|
||||
ServiceInstallCtx, ServiceLevel, ServiceStartCtx, ServiceStatus, ServiceStatusCtx,
|
||||
ServiceStopCtx, ServiceUninstallCtx,
|
||||
};
|
||||
|
||||
use winreg::{enums::*, RegKey};
|
||||
|
||||
use crate::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct WinServiceInstallOptions {
|
||||
pub dependencies: Option<Vec<String>>,
|
||||
pub description: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
}
|
||||
|
||||
pub struct WinServiceManager {
|
||||
service_manager: ServiceManager,
|
||||
}
|
||||
|
||||
fn generate_service_info(ctx: &ServiceInstallCtx) -> (ServiceInfo, Option<OsString>) {
|
||||
let start_type = if ctx.autostart {
|
||||
ServiceStartType::AutoStart
|
||||
} else {
|
||||
ServiceStartType::OnDemand
|
||||
};
|
||||
let srv_name = OsString::from(ctx.label.to_qualified_name());
|
||||
let mut dis_name = srv_name.clone();
|
||||
let mut description: Option<OsString> = None;
|
||||
let mut dependencies = Vec::<ServiceDependency>::new();
|
||||
|
||||
if let Some(s) = ctx.contents.as_ref() {
|
||||
let options: WinServiceInstallOptions = serde_json::from_str(s.as_str()).unwrap();
|
||||
if let Some(d) = options.dependencies {
|
||||
dependencies = d
|
||||
.iter()
|
||||
.map(|dep| ServiceDependency::Service(OsString::from(dep.clone())))
|
||||
.collect::<Vec<_>>();
|
||||
}
|
||||
if let Some(d) = options.description {
|
||||
description = Some(OsString::from(d));
|
||||
}
|
||||
if let Some(d) = options.display_name {
|
||||
dis_name = OsString::from(d);
|
||||
}
|
||||
}
|
||||
|
||||
let service_info = ServiceInfo {
|
||||
name: srv_name,
|
||||
display_name: dis_name,
|
||||
service_type: ServiceType::OWN_PROCESS,
|
||||
start_type,
|
||||
error_control: ServiceErrorControl::Normal,
|
||||
executable_path: ctx.program.clone(),
|
||||
launch_arguments: ctx.args.clone(),
|
||||
dependencies: dependencies.clone(),
|
||||
account_name: None,
|
||||
account_password: None,
|
||||
};
|
||||
|
||||
(service_info, description)
|
||||
}
|
||||
|
||||
impl WinServiceManager {
|
||||
pub fn new() -> Result<Self, anyhow::Error> {
|
||||
let service_manager =
|
||||
ServiceManager::local_computer(None::<&str>, ServiceManagerAccess::ALL_ACCESS)?;
|
||||
Ok(Self { service_manager })
|
||||
}
|
||||
}
|
||||
impl service_manager::ServiceManager for WinServiceManager {
|
||||
fn available(&self) -> io::Result<bool> {
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn install(&self, ctx: ServiceInstallCtx) -> io::Result<()> {
|
||||
let (service_info, description) = generate_service_info(&ctx);
|
||||
|
||||
let service = self
|
||||
.service_manager
|
||||
.create_service(&service_info, ServiceAccess::ALL_ACCESS)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
if let Some(s) = description {
|
||||
service
|
||||
.set_description(s.clone())
|
||||
.map_err(io::Error::other)?;
|
||||
}
|
||||
|
||||
if let Some(work_dir) = ctx.working_directory {
|
||||
set_service_work_directory(&ctx.label.to_qualified_name(), work_dir)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn uninstall(&self, ctx: ServiceUninstallCtx) -> io::Result<()> {
|
||||
let service = self
|
||||
.service_manager
|
||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
service.delete().map_err(io::Error::other)
|
||||
}
|
||||
|
||||
fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> {
|
||||
let service = self
|
||||
.service_manager
|
||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
service.start(&[] as &[&OsStr]).map_err(io::Error::other)
|
||||
}
|
||||
|
||||
fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> {
|
||||
let service = self
|
||||
.service_manager
|
||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
_ = service.stop().map_err(io::Error::other)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn level(&self) -> ServiceLevel {
|
||||
ServiceLevel::System
|
||||
}
|
||||
|
||||
fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> {
|
||||
match level {
|
||||
ServiceLevel::System => Ok(()),
|
||||
_ => Err(io::Error::other("Unsupported service level")),
|
||||
}
|
||||
}
|
||||
|
||||
fn status(&self, ctx: ServiceStatusCtx) -> io::Result<ServiceStatus> {
|
||||
let service = match self
|
||||
.service_manager
|
||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::QUERY_STATUS)
|
||||
{
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
if let windows_service::Error::Winapi(ref win_err) = e {
|
||||
if win_err.raw_os_error() == Some(0x424) {
|
||||
return Ok(ServiceStatus::NotInstalled);
|
||||
}
|
||||
}
|
||||
return Err(io::Error::other(e));
|
||||
}
|
||||
};
|
||||
|
||||
let status = service.query_status().map_err(io::Error::other)?;
|
||||
|
||||
match status.current_state {
|
||||
windows_service::service::ServiceState::Stopped => Ok(ServiceStatus::Stopped(None)),
|
||||
_ => Ok(ServiceStatus::Running),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl super::ServiceManager for WinServiceManager {
|
||||
fn update(&self, ctx: service_manager::ServiceInstallCtx) -> io::Result<()> {
|
||||
let (service_info, description) = generate_service_info(&ctx);
|
||||
|
||||
let service = self
|
||||
.service_manager
|
||||
.open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
service
|
||||
.change_config(&service_info)
|
||||
.map_err(io::Error::other)?;
|
||||
|
||||
if let Some(s) = description {
|
||||
service
|
||||
.set_description(s.clone())
|
||||
.map_err(io::Error::other)?;
|
||||
}
|
||||
|
||||
if let Some(work_dir) = ctx.working_directory {
|
||||
set_service_work_directory(&ctx.label.to_qualified_name(), work_dir)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn set_service_work_directory(service_name: &str, work_directory: PathBuf) -> io::Result<()> {
|
||||
let (reg_key, _) =
|
||||
RegKey::predef(HKEY_LOCAL_MACHINE).create_subkey(WIN_SERVICE_WORK_DIR_REG_KEY)?;
|
||||
reg_key
|
||||
.set_value::<OsString, _>(service_name, &work_directory.as_os_str().to_os_string())?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ use crate::{
|
||||
set_default_machine_id, stun::MockStunInfoCollector,
|
||||
},
|
||||
connector::create_connector_by_url,
|
||||
instance_manager::{NetworkInstanceManager, WebClientGuard},
|
||||
instance_manager::{DaemonGuard, NetworkInstanceManager},
|
||||
proto::common::NatType,
|
||||
tunnel::{IpVersion, TunnelConnector},
|
||||
};
|
||||
@@ -19,7 +19,7 @@ pub mod session;
|
||||
pub struct WebClient {
|
||||
controller: Arc<controller::Controller>,
|
||||
tasks: ScopedTask<()>,
|
||||
manager_guard: WebClientGuard,
|
||||
manager_guard: DaemonGuard,
|
||||
}
|
||||
|
||||
impl WebClient {
|
||||
@@ -29,7 +29,7 @@ impl WebClient {
|
||||
hostname: H,
|
||||
manager: Arc<NetworkInstanceManager>,
|
||||
) -> Self {
|
||||
let manager_guard = manager.register_web_client();
|
||||
let manager_guard = manager.register_daemon();
|
||||
let controller = Arc::new(controller::Controller::new(
|
||||
token.to_string(),
|
||||
hostname.to_string(),
|
||||
|
||||
@@ -32,6 +32,11 @@
|
||||
rustVersion = "1.89.0";
|
||||
makeRust =
|
||||
features:
|
||||
let
|
||||
rustTarget = pkgs.stdenv.hostPlatform.config;
|
||||
muslTarget = pkgs.lib.replaceStrings [ "gnu" ] [ "musl" ] rustTarget;
|
||||
muslTargets = if pkgs.stdenv.isLinux then [ muslTarget ] else [ ];
|
||||
in
|
||||
pkgs.rust-bin.stable.${rustVersion}.default.override {
|
||||
extensions = [
|
||||
"rust-src"
|
||||
@@ -39,7 +44,7 @@
|
||||
]
|
||||
++ (if builtins.elem "android" features then android.rust.extensions else [ ]);
|
||||
|
||||
targets = if builtins.elem "android" features then android.rust.targets else [ ];
|
||||
targets = muslTargets ++ (if builtins.elem "android" features then android.rust.targets else []);
|
||||
};
|
||||
|
||||
android = import ./android.nix {
|
||||
@@ -76,6 +81,7 @@
|
||||
);
|
||||
|
||||
buildInputs = with pkgs; ([
|
||||
jemalloc
|
||||
zstd
|
||||
openssl
|
||||
libclang
|
||||
@@ -90,6 +96,7 @@
|
||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath (flattenPaths (buildInputs ++ nativeBuildInputs));
|
||||
ZSTD_SYS_USE_PKG_CONFIG = true;
|
||||
KCP_SYS_EXTRA_HEADER_PATH = "${pkgs.libclang.lib}/lib/clang/19/include:${pkgs.glibc.dev}/include";
|
||||
JEMALLOC_OVERRIDE = "${pkgs.jemalloc}/lib/libjemalloc.so";
|
||||
}
|
||||
// (if hasFeature "android" then android.envVars else { }));
|
||||
in
|
||||
|
||||
Reference in New Issue
Block a user