From 6e77e6b5e790f62fa017fca4b1cbf1eca9f1a9c6 Mon Sep 17 00:00:00 2001 From: "Sijie.Sun" Date: Tue, 4 Jun 2024 23:06:10 +0800 Subject: [PATCH] support start on reboot (#132) * move launcher to eastier lib * support auto start after reboot --- Cargo.lock | 42 +++++ easytier-gui/locales/cn.yml | 2 + easytier-gui/locales/en.yml | 2 + easytier-gui/src-tauri/Cargo.toml | 6 +- easytier-gui/src-tauri/src/main.rs | 148 +++++++++--------- easytier-gui/src/auto-imports.d.ts | 6 + easytier-gui/src/composables/network.ts | 4 + easytier-gui/src/main.ts | 2 + easytier-gui/src/modules/auto_launch.ts | 16 ++ easytier-gui/src/pages/index.vue | 67 ++++---- easytier-gui/src/stores/network.ts | 15 ++ easytier/Cargo.toml | 2 +- easytier/src/common/ifcfg.rs | 1 + .../src-tauri => easytier}/src/launcher.rs | 70 ++++++++- easytier/src/lib.rs | 16 +- 15 files changed, 281 insertions(+), 118 deletions(-) create mode 100644 easytier-gui/src/modules/auto_launch.ts rename {easytier-gui/src-tauri => easytier}/src/launcher.rs (78%) diff --git a/Cargo.lock b/Cargo.lock index 31598d0..cc8ac69 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -247,6 +247,17 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9a3820bc9e9aaf60c8389c2a4808548599f4ff254ce6bdb608ac3631d4ad76" +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs", + "thiserror", + "winreg 0.10.1", +] + [[package]] name = "auto_impl" version = "1.2.0" @@ -1128,6 +1139,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -1138,6 +1158,17 @@ dependencies = [ "dirs-sys-next", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + [[package]] name = "dirs-sys-next" version = "0.1.2" @@ -1260,8 +1291,10 @@ name = "easytier-gui" version = "0.0.0" dependencies = [ "anyhow", + "auto-launch", "chrono", "dashmap", + "dunce", "easytier", "gethostname", "once_cell", @@ -6260,6 +6293,15 @@ 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" diff --git a/easytier-gui/locales/cn.yml b/easytier-gui/locales/cn.yml index 17070b1..bc1a664 100644 --- a/easytier-gui/locales/cn.yml +++ b/easytier-gui/locales/cn.yml @@ -32,6 +32,8 @@ retain_network_instance: 保留网络实例 collect_network_infos: 收集网络信息 settings: 设置 exchange_language: Switch to English +disable_auto_launch: 关闭开机自启 +enable_auto_launch: 开启开机自启 exit: 退出 chips_placeholder: 例如: {0}, 按回车添加 hostname_placeholder: '留空默认为主机名: {0}' diff --git a/easytier-gui/locales/en.yml b/easytier-gui/locales/en.yml index 72ac2b4..8b122c0 100644 --- a/easytier-gui/locales/en.yml +++ b/easytier-gui/locales/en.yml @@ -32,6 +32,8 @@ retain_network_instance: Retain Network Instance collect_network_infos: Collect Network Infos settings: Settings exchange_language: 切换中文 +disable_auto_launch: Disable Launch on Reboot +enable_auto_launch: Enable Launch on Reboot exit: Exit chips_placeholder: 'e.g: {0}, press Enter to add' diff --git a/easytier-gui/src-tauri/Cargo.toml b/easytier-gui/src-tauri/Cargo.toml index bf702f8..a03b397 100644 --- a/easytier-gui/src-tauri/Cargo.toml +++ b/easytier-gui/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "easytier-gui" version = "0.0.0" -description = "A Tauri App" +description = "EasyTier GUI" authors = ["you"] edition = "2021" @@ -29,6 +29,10 @@ dashmap = "5.5.3" privilege = "0.3" gethostname = "0.4.3" + +auto-launch = "0.5.0" +dunce = "1.0.4" + [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! custom-protocol = ["tauri/custom-protocol"] diff --git a/easytier-gui/src-tauri/src/main.rs b/easytier-gui/src-tauri/src/main.rs index be19eca..2967f8d 100644 --- a/easytier-gui/src-tauri/src/main.rs +++ b/easytier-gui/src-tauri/src/main.rs @@ -1,22 +1,17 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] -mod launcher; - use std::{collections::BTreeMap, env::current_exe, process}; use anyhow::Context; -use chrono::{DateTime, Local}; +use auto_launch::AutoLaunchBuilder; use dashmap::DashMap; use easytier::{ - common::{ - config::{ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader, VpnPortalConfig}, - global_ctx::GlobalCtxEvent, + common::config::{ + ConfigLoader, NetworkIdentity, PeerConfig, TomlConfigLoader, VpnPortalConfig, }, - rpc::{PeerInfo, Route}, - utils::{list_peer_route_pair, PeerRoutePair}, + launcher::{NetworkInstance, NetworkInstanceRunningInfo}, }; -use launcher::{EasyTierLauncher, MyNodeInfo}; use serde::{Deserialize, Serialize}; use tauri::{ @@ -167,71 +162,6 @@ impl NetworkConfig { } } -#[derive(Deserialize, Serialize, Debug)] -struct NetworkInstanceRunningInfo { - my_node_info: MyNodeInfo, - events: Vec<(DateTime, GlobalCtxEvent)>, - node_info: MyNodeInfo, - routes: Vec, - peers: Vec, - peer_route_pairs: Vec, - running: bool, - error_msg: Option, -} - -struct NetworkInstance { - config: TomlConfigLoader, - launcher: Option, -} - -impl NetworkInstance { - fn new(cfg: NetworkConfig) -> Result { - Ok(Self { - config: cfg.gen_config()?, - launcher: None, - }) - } - - fn is_easytier_running(&self) -> bool { - self.launcher.is_some() && self.launcher.as_ref().unwrap().running() - } - - fn get_running_info(&self) -> Option { - if self.launcher.is_none() { - return None; - } - - let launcher = self.launcher.as_ref().unwrap(); - - let peers = launcher.get_peers(); - let routes = launcher.get_routes(); - let peer_route_pairs = list_peer_route_pair(peers.clone(), routes.clone()); - - Some(NetworkInstanceRunningInfo { - my_node_info: launcher.get_node_info(), - events: launcher.get_events(), - node_info: launcher.get_node_info(), - routes, - peers, - peer_route_pairs, - running: launcher.running(), - error_msg: launcher.error_msg(), - }) - } - - fn start(&mut self) -> Result<(), anyhow::Error> { - if self.is_easytier_running() { - return Ok(()); - } - - let mut launcher = EasyTierLauncher::new(); - launcher.start(|| Ok(self.config.clone())); - - self.launcher = Some(launcher); - Ok(()) - } -} - static INSTANCE_MAP: once_cell::sync::Lazy> = once_cell::sync::Lazy::new(DashMap::new); @@ -249,7 +179,8 @@ fn run_network_instance(cfg: NetworkConfig) -> Result<(), String> { } let instance_id = cfg.instance_id.clone(); - let mut instance = NetworkInstance::new(cfg).map_err(|e| e.to_string())?; + let cfg = cfg.gen_config().map_err(|e| e.to_string())?; + let mut instance = NetworkInstance::new(cfg); instance.start().map_err(|e| e.to_string())?; println!("instance {} started", instance_id); @@ -286,6 +217,11 @@ fn get_os_hostname() -> Result { Ok(gethostname::gethostname().to_string_lossy().to_string()) } +#[tauri::command] +fn set_auto_launch_status(app_handle: tauri::AppHandle, enable: bool) -> Result { + Ok(init_launch(&app_handle, enable).map_err(|e| e.to_string())?) +} + fn toggle_window_visibility(window: &Window) { if window.is_visible().unwrap() { window.hide().unwrap(); @@ -307,6 +243,65 @@ fn check_sudo() -> bool { is_elevated } +/// init the auto launch +pub fn init_launch(_app_handle: &tauri::AppHandle, enable: bool) -> Result { + let app_exe = current_exe()?; + let app_exe = dunce::canonicalize(app_exe)?; + let app_name = app_exe + .file_stem() + .and_then(|f| f.to_str()) + .ok_or(anyhow::anyhow!("failed to get file stem"))?; + + let app_path = app_exe + .as_os_str() + .to_str() + .ok_or(anyhow::anyhow!("failed to get app_path"))? + .to_string(); + + #[cfg(target_os = "windows")] + let app_path = format!("\"{app_path}\""); + + // use the /Applications/easytier-gui.app + #[cfg(target_os = "macos")] + let app_path = (|| -> Option { + let path = std::path::PathBuf::from(&app_path); + let path = path.parent()?.parent()?.parent()?; + let extension = path.extension()?.to_str()?; + match extension == "app" { + true => Some(path.as_os_str().to_str()?.to_string()), + false => None, + } + })() + .unwrap_or(app_path); + + #[cfg(target_os = "linux")] + let app_path = { + let appimage = _app_handle.env().appimage; + appimage + .and_then(|p| p.to_str().map(|s| s.to_string())) + .unwrap_or(app_path) + }; + + let auto = AutoLaunchBuilder::new() + .set_app_name(app_name) + .set_app_path(&app_path) + .build() + .with_context(|| "failed to build auto launch")?; + + if enable && !auto.is_enabled().unwrap_or(false) { + // 避免重复设置登录项 + let _ = auto.disable(); + auto.enable() + .with_context(|| "failed to enable auto launch")? + } else if !enable { + let _ = auto.disable(); + } + + let enabled = auto.is_enabled()?; + + Ok(enabled) +} + fn main() { if !check_sudo() { process::exit(0); @@ -324,7 +319,8 @@ fn main() { run_network_instance, retain_network_instance, collect_network_infos, - get_os_hostname + get_os_hostname, + set_auto_launch_status ]) .system_tray(SystemTray::new().with_menu(tray_menu)) .on_system_tray_event(|app, event| match event { diff --git a/easytier-gui/src/auto-imports.d.ts b/easytier-gui/src/auto-imports.d.ts index d337cfb..f864211 100644 --- a/easytier-gui/src/auto-imports.d.ts +++ b/easytier-gui/src/auto-imports.d.ts @@ -27,6 +27,7 @@ declare global { const isReactive: typeof import('vue')['isReactive'] const isReadonly: typeof import('vue')['isReadonly'] const isRef: typeof import('vue')['isRef'] + const loadRunningInstanceIdsFromLocalStorage: typeof import('./stores/network')['loadRunningInstanceIdsFromLocalStorage'] const mapActions: typeof import('pinia')['mapActions'] const mapGetters: typeof import('pinia')['mapGetters'] const mapState: typeof import('pinia')['mapState'] @@ -58,6 +59,7 @@ declare global { const retainNetworkInstance: typeof import('./composables/network')['retainNetworkInstance'] const runNetworkInstance: typeof import('./composables/network')['runNetworkInstance'] const setActivePinia: typeof import('pinia')['setActivePinia'] + const setAutoLaunchStatus: typeof import('./composables/network')['setAutoLaunchStatus'] const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] const shallowReactive: typeof import('vue')['shallowReactive'] const shallowReadonly: typeof import('vue')['shallowReadonly'] @@ -116,6 +118,7 @@ declare module 'vue' { readonly isReactive: UnwrapRef readonly isReadonly: UnwrapRef readonly isRef: UnwrapRef + readonly loadRunningInstanceIdsFromLocalStorage: UnwrapRef readonly mapActions: UnwrapRef readonly mapGetters: UnwrapRef readonly mapState: UnwrapRef @@ -147,6 +150,7 @@ declare module 'vue' { readonly retainNetworkInstance: UnwrapRef readonly runNetworkInstance: UnwrapRef readonly setActivePinia: UnwrapRef + readonly setAutoLaunchStatus: UnwrapRef readonly setMapStoreSuffix: UnwrapRef readonly shallowReactive: UnwrapRef readonly shallowReadonly: UnwrapRef @@ -198,6 +202,7 @@ declare module '@vue/runtime-core' { readonly isReactive: UnwrapRef readonly isReadonly: UnwrapRef readonly isRef: UnwrapRef + readonly loadRunningInstanceIdsFromLocalStorage: UnwrapRef readonly mapActions: UnwrapRef readonly mapGetters: UnwrapRef readonly mapState: UnwrapRef @@ -229,6 +234,7 @@ declare module '@vue/runtime-core' { readonly retainNetworkInstance: UnwrapRef readonly runNetworkInstance: UnwrapRef readonly setActivePinia: UnwrapRef + readonly setAutoLaunchStatus: UnwrapRef readonly setMapStoreSuffix: UnwrapRef readonly shallowReactive: UnwrapRef readonly shallowReadonly: UnwrapRef diff --git a/easytier-gui/src/composables/network.ts b/easytier-gui/src/composables/network.ts index 7dbbd2d..bc4ba6d 100644 --- a/easytier-gui/src/composables/network.ts +++ b/easytier-gui/src/composables/network.ts @@ -20,3 +20,7 @@ export async function collectNetworkInfos() { export async function getOsHostname() { return await invoke('get_os_hostname') } + +export async function setAutoLaunchStatus(enable: boolean) { + return await invoke('set_auto_launch_status', { enable }) +} diff --git a/easytier-gui/src/main.ts b/easytier-gui/src/main.ts index d16bd00..8576287 100644 --- a/easytier-gui/src/main.ts +++ b/easytier-gui/src/main.ts @@ -10,6 +10,7 @@ import 'primevue/resources/themes/aura-light-green/theme.css' import 'primeicons/primeicons.css' import 'primeflex/primeflex.css' import { i18n, loadLanguageAsync } from '~/modules/i18n' +import { loadAutoLaunchStatusAsync, getAutoLaunchStatusAsync } from './modules/auto_launch' if (import.meta.env.PROD) { document.addEventListener('keydown', (event) => { @@ -28,6 +29,7 @@ if (import.meta.env.PROD) { async function main() { await loadLanguageAsync(localStorage.getItem('lang') || 'en') + await loadAutoLaunchStatusAsync(getAutoLaunchStatusAsync()) const app = createApp(App) diff --git a/easytier-gui/src/modules/auto_launch.ts b/easytier-gui/src/modules/auto_launch.ts new file mode 100644 index 0000000..8b12fb5 --- /dev/null +++ b/easytier-gui/src/modules/auto_launch.ts @@ -0,0 +1,16 @@ +import { setAutoLaunchStatus } from "~/composables/network" + +export async function loadAutoLaunchStatusAsync(enable: boolean): Promise { + try { + const ret = await setAutoLaunchStatus(enable) + localStorage.setItem('auto_launch', JSON.stringify(ret)) + return ret + } catch (e) { + console.error(e) + return false + } +} + +export function getAutoLaunchStatusAsync(): boolean { + return localStorage.getItem('auto_launch') === 'true' +} diff --git a/easytier-gui/src/pages/index.vue b/easytier-gui/src/pages/index.vue index e3ddf91..c78ca04 100644 --- a/easytier-gui/src/pages/index.vue +++ b/easytier-gui/src/pages/index.vue @@ -10,6 +10,8 @@ import Status from '~/components/Status.vue' import type { NetworkConfig } from '~/types/network' import { loadLanguageAsync } from '~/modules/i18n' +import { getAutoLaunchStatusAsync as getAutoLaunchStatus, loadAutoLaunchStatusAsync } from '~/modules/auto_launch' +import { loadRunningInstanceIdsFromLocalStorage } from '~/stores/network' const { t, locale } = useI18n() const visible = ref(false) @@ -63,6 +65,7 @@ function addNewNetwork() { networkStore.$subscribe(async () => { networkStore.saveToLocalStorage() + networkStore.saveRunningInstanceIdsToLocalStorage() try { await parseNetworkConfig(networkStore.curNetwork) messageBarSeverity.value = Severity.None @@ -123,9 +126,16 @@ const setting_menu_items = ref([ await loadLanguageAsync((locale.value === 'en' ? 'cn' : 'en')) }, }, + { + label: () => getAutoLaunchStatus() ? t('disable_auto_launch') : t('enable_auto_launch'), + icon: 'pi pi-desktop', + command: async () => { + await loadAutoLaunchStatusAsync(!getAutoLaunchStatus()) + }, + }, { label: () => t('exit'), - icon: 'pi pi-times', + icon: 'pi pi-power-off', command: async () => { await exit(1) }, @@ -140,6 +150,16 @@ function toggle_setting_menu(event: any) { onMounted(async () => { networkStore.loadFromLocalStorage() + if (getAutoLaunchStatus()) { + let prev_running_ids = loadRunningInstanceIdsFromLocalStorage() + for (let id of prev_running_ids) { + let cfg = networkStore.networkList.find((item) => item.instance_id === id) + if (cfg) { + networkStore.addNetworkInstance(cfg.instance_id) + await runNetworkInstance(cfg) + } + } + } }) function isRunning(id: string) { @@ -168,35 +188,28 @@ function isRunning(id: string) { diff --git a/easytier-gui/src/stores/network.ts b/easytier-gui/src/stores/network.ts index 8305ff9..6640128 100644 --- a/easytier-gui/src/stores/network.ts +++ b/easytier-gui/src/stores/network.ts @@ -70,6 +70,7 @@ export const useNetworkStore = defineStore('networkStore', { this.instances[instanceId].error_msg = info.error_msg || '' this.instances[instanceId].detail = info } + this.saveRunningInstanceIdsToLocalStorage() }, loadFromLocalStorage() { @@ -92,8 +93,22 @@ export const useNetworkStore = defineStore('networkStore', { saveToLocalStorage() { localStorage.setItem('networkList', JSON.stringify(this.networkList)) }, + + saveRunningInstanceIdsToLocalStorage() { + let instance_ids = Object.keys(this.instances).filter((instanceId) => this.instances[instanceId].running) + localStorage.setItem('runningInstanceIds', JSON.stringify(instance_ids)) + } }, }) if (import.meta.hot) import.meta.hot.accept(acceptHMRUpdate(useNetworkStore as any, import.meta.hot)) + +export function loadRunningInstanceIdsFromLocalStorage(): string[] { + try { + return JSON.parse(localStorage.getItem('runningInstanceIds') || '[]') + } catch (e) { + console.error(e) + return [] + } +} diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 75f9591..9b922e2 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -42,7 +42,7 @@ auto_impl = "1.1.0" crossbeam = "0.8.4" time = "0.3" toml = "0.8.12" -chrono = "0.4.35" +chrono = { version = "0.4.37", features = ["serde"] } gethostname = "0.4.3" diff --git a/easytier/src/common/ifcfg.rs b/easytier/src/common/ifcfg.rs index 298d1a5..12ab16f 100644 --- a/easytier/src/common/ifcfg.rs +++ b/easytier/src/common/ifcfg.rs @@ -57,6 +57,7 @@ async fn run_shell_cmd(cmd: &str) -> Result<(), Error> { { const CREATE_NO_WINDOW: u32 = 0x08000000; cmd_out = Command::new("cmd") + .stdin(std::process::Stdio::null()) .arg("/C") .arg(cmd) .creation_flags(CREATE_NO_WINDOW) diff --git a/easytier-gui/src-tauri/src/launcher.rs b/easytier/src/launcher.rs similarity index 78% rename from easytier-gui/src-tauri/src/launcher.rs rename to easytier/src/launcher.rs index 9537660..c852c70 100644 --- a/easytier-gui/src-tauri/src/launcher.rs +++ b/easytier/src/launcher.rs @@ -3,8 +3,7 @@ use std::{ sync::{atomic::AtomicBool, Arc, RwLock}, }; -use chrono::{DateTime, Local}; -use easytier::{ +use crate::{ common::{ config::{ConfigLoader, TomlConfigLoader}, global_ctx::GlobalCtxEvent, @@ -16,7 +15,9 @@ use easytier::{ cli::{PeerInfo, Route, StunInfo}, peer::GetIpListResponse, }, + utils::{list_peer_route_pair, PeerRoutePair}, }; +use chrono::{DateTime, Local}; use serde::{Deserialize, Serialize}; #[derive(Default, Clone, Debug, Serialize, Deserialize)] @@ -212,3 +213,68 @@ impl Drop for EasyTierLauncher { } } } + +#[derive(Deserialize, Serialize, Debug)] +pub struct NetworkInstanceRunningInfo { + pub my_node_info: MyNodeInfo, + pub events: Vec<(DateTime, GlobalCtxEvent)>, + pub node_info: MyNodeInfo, + pub routes: Vec, + pub peers: Vec, + pub peer_route_pairs: Vec, + pub running: bool, + pub error_msg: Option, +} + +pub struct NetworkInstance { + config: TomlConfigLoader, + launcher: Option, +} + +impl NetworkInstance { + pub fn new(config: TomlConfigLoader) -> Self { + Self { + config, + launcher: None, + } + } + + pub fn is_easytier_running(&self) -> bool { + self.launcher.is_some() && self.launcher.as_ref().unwrap().running() + } + + pub fn get_running_info(&self) -> Option { + if self.launcher.is_none() { + return None; + } + + let launcher = self.launcher.as_ref().unwrap(); + + let peers = launcher.get_peers(); + let routes = launcher.get_routes(); + let peer_route_pairs = list_peer_route_pair(peers.clone(), routes.clone()); + + Some(NetworkInstanceRunningInfo { + my_node_info: launcher.get_node_info(), + events: launcher.get_events(), + node_info: launcher.get_node_info(), + routes, + peers, + peer_route_pairs, + running: launcher.running(), + error_msg: launcher.error_msg(), + }) + } + + pub fn start(&mut self) -> Result<(), anyhow::Error> { + if self.is_easytier_running() { + return Ok(()); + } + + let mut launcher = EasyTierLauncher::new(); + launcher.start(|| Ok(self.config.clone())); + + self.launcher = Some(launcher); + Ok(()) + } +} diff --git a/easytier/src/lib.rs b/easytier/src/lib.rs index 1653e9a..b63aec6 100644 --- a/easytier/src/lib.rs +++ b/easytier/src/lib.rs @@ -1,13 +1,15 @@ #![allow(dead_code)] -pub mod arch; +mod arch; +mod connector; +mod gateway; +mod instance; +mod peer_center; +mod peers; +mod vpn_portal; + pub mod common; -pub mod connector; -pub mod gateway; -pub mod instance; -pub mod peer_center; -pub mod peers; +pub mod launcher; pub mod rpc; pub mod tunnel; pub mod utils; -pub mod vpn_portal;