diff --git a/Cargo.lock b/Cargo.lock index 4c43aa2..fa04a67 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1191,6 +1191,15 @@ dependencies = [ "objc", ] +[[package]] +name = "codepage" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48f68d061bc2828ae826206326e61251aca94c1e4a5305cf52d9138639c918b4" +dependencies = [ + "encoding_rs", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -1866,6 +1875,7 @@ dependencies = [ "serde", "serde_json", "serial_test", + "service-manager", "smoltcp", "socket2", "stun_codec", @@ -2063,6 +2073,17 @@ dependencies = [ "encoding_index_tests", ] +[[package]] +name = "encoding-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87b881ab2524b96a5ce932056c7482ba6152e2226fed3936b3e592adeb95ca6d" +dependencies = [ + "codepage", + "encoding_rs", + "windows-sys 0.52.0", +] + [[package]] name = "encoding_index_tests" version = "0.1.4" @@ -6543,6 +6564,20 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "service-manager" +version = "0.7.1" +source = "git+https://github.com/chipsenkbeil/service-manager-rs.git?branch=main#13dae5e8160f91fdc9834d847165cc5ce0a72fb3" +dependencies = [ + "cfg-if", + "dirs 4.0.0", + "encoding-utils", + "encoding_rs", + "plist", + "which", + "xml-rs", +] + [[package]] name = "servo_arc" version = "0.1.1" @@ -9339,6 +9374,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "xml-rs" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4e2e2f7cba5a093896c1e150fbfe177d1883e7448200efb81d40b9d339ef26" + [[package]] name = "yansi" version = "1.0.1" diff --git a/easytier/Cargo.toml b/easytier/Cargo.toml index 57e6146..21de40e 100644 --- a/easytier/Cargo.toml +++ b/easytier/Cargo.toml @@ -181,6 +181,8 @@ 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"} + [target.'cfg(any(target_os = "linux", target_os = "macos", target_os = "windows", target_os = "freebsd"))'.dependencies] machine-uid = "0.5.3" diff --git a/easytier/locales/app.yml b/easytier/locales/app.yml index 7519036..4941bf9 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -120,7 +120,7 @@ core_clap: ipv6_listener: en: "the url of the ipv6 listener, e.g.: tcp://[::]:11010, if not set, will listen on random udp port" zh-CN: "IPv6 监听器的URL,例如:tcp://[::]:11010,如果未设置,将在随机UDP端口上监听" - + core_app: panic_backtrace_save: en: "backtrace saved to easytier-panic.log" diff --git a/easytier/src/arch/windows.rs b/easytier/src/arch/windows.rs index 2ee8452..cf631ba 100644 --- a/easytier/src/arch/windows.rs +++ b/easytier/src/arch/windows.rs @@ -152,4 +152,4 @@ pub fn setup_socket_for_win( } Ok(()) -} +} \ No newline at end of file diff --git a/easytier/src/common/constants.rs b/easytier/src/common/constants.rs index 3b86dba..fe561f2 100644 --- a/easytier/src/common/constants.rs +++ b/easytier/src/common/constants.rs @@ -25,6 +25,8 @@ define_global_var!(OSPF_UPDATE_MY_GLOBAL_FOREIGN_NETWORK_INTERVAL_SEC, u64, 10); pub const UDP_HOLE_PUNCH_CONNECTOR_SERVICE_ID: u32 = 2; +pub const WIN_SERVICE_WORK_DIR_REG_KEY: &str = "SOFTWARE\\EasyTier\\Service\\WorkDir"; + pub const EASYTIER_VERSION: &str = git_version::git_version!( args = ["--abbrev=8", "--always", "--dirty=~"], prefix = concat!(env!("CARGO_PKG_VERSION"), "-"), diff --git a/easytier/src/easytier-cli.rs b/easytier/src/easytier-cli.rs index 943ed67..5c32884 100644 --- a/easytier/src/easytier-cli.rs +++ b/easytier/src/easytier-cli.rs @@ -1,8 +1,11 @@ -use std::{net::SocketAddr, sync::Mutex, time::Duration, vec}; +use std::{ + ffi::OsString, fmt::Write, net::SocketAddr, path::PathBuf, sync::Mutex, time::Duration, vec, +}; use anyhow::{Context, Ok}; use clap::{command, Args, Parser, Subcommand}; use humansize::format_size; +use service_manager::*; use tabled::settings::Style; use tokio::time::timeout; @@ -54,6 +57,7 @@ enum SubCommand { PeerCenter, VpnPortal, Node(NodeArgs), + Service(ServiceArgs), } #[derive(Args, Debug)] @@ -120,6 +124,45 @@ struct NodeArgs { sub_command: Option, } +#[derive(Args, Debug)] +struct ServiceArgs { + #[arg(short, long, default_value = env!("CARGO_PKG_NAME"), help = "service name")] + name: String, + + #[command(subcommand)] + sub_command: ServiceSubCommand, +} + +#[derive(Subcommand, Debug)] +enum ServiceSubCommand { + Install(InstallArgs), + Uninstall, + Status, + Start, + Stop, +} + +#[derive(Args, Debug)] +struct InstallArgs { + #[arg(long, default_value = env!("CARGO_PKG_DESCRIPTION"), help = "service description")] + description: String, + + #[arg(long)] + display_name: Option, + + #[arg(long, default_value = "false")] + disable_autostart: bool, + + #[arg(long)] + core_path: Option, + + #[arg(long)] + service_work_dir: Option, + + #[arg(trailing_var_arg = true, allow_hyphen_values = true)] + core_args: Option>, +} + type Error = anyhow::Error; struct CommandHandler { @@ -476,6 +519,257 @@ impl CommandHandler { } } +pub struct ServiceInstallOptions { + pub program: PathBuf, + pub args: Vec, + pub work_directory: PathBuf, + pub disable_autostart: bool, + pub description: Option, + pub display_name: Option, +} +pub struct Service { + lable: ServiceLabel, + kind: ServiceManagerKind, + service_manager: Box, +} + +impl Service { + pub fn new(name: String) -> Result { + #[cfg(target_os = "windows")] + let service_manager = Box::new(crate::win_service_manager::WinServiceManager::new()?); + + #[cfg(not(target_os = "windows"))] + let service_manager = ::native()?; + let kind = ServiceManagerKind::native()?; + + 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, + }; + if self.status()? != ServiceStatus::NotInstalled { + return Err(anyhow::anyhow!("Service is already installed")); + } + + self.service_manager + .install(ctx) + .map_err(|e| anyhow::anyhow!("failed to install service: {}", e)) + } + + 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 { + 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 { + 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()), + _ => { + #[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 { + let args = options + .args + .iter() + .map(|a| a.to_string_lossy()) + .collect::>() + .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, "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 { + let name = self.lable.to_qualified_name(); + let args = options + .args + .iter() + .map(|a| a.to_string_lossy()) + .collect::>() + .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 { + let args = options + .args + .iter() + .map(|a| a.to_string_lossy()) + .collect::>() + .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) + } +} + #[tokio::main] #[tracing::instrument] async fn main() -> Result<(), Error> { @@ -638,7 +932,265 @@ async fn main() -> Result<(), Error> { } } } + SubCommand::Service(service_args) => { + let service = Service::new(service_args.name)?; + match service_args.sub_command { + ServiceSubCommand::Install(install_args) => { + let bin_path = install_args.core_path.unwrap_or_else(|| { + let mut ret = std::env::current_exe() + .unwrap() + .parent() + .unwrap() + .join("easytier-core"); + + if cfg!(target_os = "windows") { + ret.set_extension("exe"); + } + + ret + }); + let bin_path = std::fs::canonicalize(bin_path).map_err(|e| { + anyhow::anyhow!("failed to get easytier core application: {}", e) + })?; + let bin_args = install_args.core_args.unwrap_or_default(); + let work_dir = install_args.service_work_dir.unwrap_or_else(|| { + if cfg!(target_os = "windows") { + bin_path.parent().unwrap().to_path_buf() + } else { + std::env::temp_dir() + } + }); + + let work_dir = std::fs::canonicalize(&work_dir).map_err(|e| { + anyhow::anyhow!( + "failed to get service work directory[{}]: {}", + work_dir.display(), + e + ) + })?; + + if !work_dir.is_dir() { + return Err(anyhow::anyhow!("work directory is not a directory")); + } + + let install_options = ServiceInstallOptions { + program: bin_path, + args: bin_args, + work_directory: work_dir, + disable_autostart: install_args.disable_autostart, + description: Some(install_args.description), + display_name: install_args.display_name, + }; + service.install(&install_options)?; + } + ServiceSubCommand::Uninstall => { + service.uninstall()?; + } + ServiceSubCommand::Status => { + let status = service.status()?; + match status { + ServiceStatus::Running => println!("Service is running"), + ServiceStatus::Stopped(_) => println!("Service is stopped"), + ServiceStatus::NotInstalled => println!("Service is not installed"), + } + } + ServiceSubCommand::Start => { + service.start()?; + } + ServiceSubCommand::Stop => { + service.stop()?; + } + } + } } 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>, + pub description: Option, + pub display_name: Option, + } + + pub struct WinServiceManager { + service_manager: ServiceManager, + } + + impl WinServiceManager { + pub fn new() -> Result { + 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 { + 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 = None; + let mut dependencies = Vec::::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::>(); + } + 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(|e| io::Error::new(io::ErrorKind::Other, e))?; + + if let Some(s) = description { + service + .set_description(s.clone()) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + } + + 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(|e| io::Error::new(io::ErrorKind::Other, e))?; + + service + .delete() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + } + + fn start(&self, ctx: ServiceStartCtx) -> io::Result<()> { + let service = self + .service_manager + .open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + service + .start(&[] as &[&OsStr]) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) + } + + fn stop(&self, ctx: ServiceStopCtx) -> io::Result<()> { + let service = self + .service_manager + .open_service(ctx.label.to_qualified_name(), ServiceAccess::ALL_ACCESS) + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + _ = service + .stop() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + Ok(()) + } + + fn level(&self) -> ServiceLevel { + ServiceLevel::System + } + + fn set_level(&mut self, level: ServiceLevel) -> io::Result<()> { + match level { + ServiceLevel::System => Ok(()), + _ => Err(io::Error::new( + io::ErrorKind::Other, + "Unsupported service level", + )), + } + } + + fn status(&self, ctx: ServiceStatusCtx) -> io::Result { + 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::new(io::ErrorKind::Other, e)); + } + }; + + let status = service + .query_status() + .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; + + 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::(service_name, &work_directory.as_os_str().to_os_string())?; + Ok(()) + } +} diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index 5258d19..3fbb310 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -294,11 +294,11 @@ struct Cli { rust_i18n::i18n!("locales", fallback = "en"); impl Cli { - fn parse_listeners(no_listener: bool, listeners: Vec) -> Vec { + fn parse_listeners(no_listener: bool, listeners: Vec) -> anyhow::Result> { let proto_port_offset = vec![("tcp", 0), ("udp", 0), ("wg", 1), ("ws", 1), ("wss", 2)]; if no_listener || listeners.is_empty() { - return vec![]; + return Ok(vec![]); } let origin_listners = listeners; @@ -308,7 +308,7 @@ impl Cli { for (proto, offset) in proto_port_offset { listeners.push(format!("{}://0.0.0.0:{}", proto, port + offset)); } - return listeners; + return Ok(listeners); } } @@ -325,7 +325,7 @@ impl Cli { .iter() .find(|(proto, _)| *proto == proto_port[0]) else { - panic!("unknown protocol: {}", proto_port[0]); + return Err(anyhow::anyhow!("unknown protocol: {}", proto_port[0])); }; let port = if proto_port.len() == 2 { @@ -338,7 +338,7 @@ impl Cli { } } - listeners + Ok(listeners) } fn check_tcp_available(port: u16) -> Option { @@ -346,66 +346,66 @@ impl Cli { TcpSocket::new_v4().unwrap().bind(s).map(|_| s).ok() } - fn parse_rpc_portal(rpc_portal: String) -> SocketAddr { + fn parse_rpc_portal(rpc_portal: String) -> anyhow::Result { if let Ok(port) = rpc_portal.parse::() { if port == 0 { // check tcp 15888 first for i in 15888..15900 { if let Some(s) = Cli::check_tcp_available(i) { - return s; + return Ok(s); } } - return "0.0.0.0:0".parse().unwrap(); + return Ok("0.0.0.0:0".parse().unwrap()); } - return format!("0.0.0.0:{}", port).parse().unwrap(); + return Ok(format!("0.0.0.0:{}", port).parse().unwrap()); } - rpc_portal.parse().unwrap() + Ok(rpc_portal.parse()?) } } -impl From for TomlConfigLoader { - fn from(cli: Cli) -> Self { +impl TryFrom<&Cli> for TomlConfigLoader { + type Error = anyhow::Error; + + fn try_from(cli: &Cli) -> Result { if let Some(config_file) = &cli.config_file { println!( "NOTICE: loading config file: {:?}, will ignore all command line flags\n", config_file ); - return TomlConfigLoader::new(config_file) - .with_context(|| format!("failed to load config file: {:?}", cli.config_file)) - .unwrap(); + return Ok(TomlConfigLoader::new(config_file) + .with_context(|| format!("failed to load config file: {:?}", cli.config_file))?); } let cfg = TomlConfigLoader::default(); - cfg.set_hostname(cli.hostname); + cfg.set_hostname(cli.hostname.clone()); - cfg.set_network_identity(NetworkIdentity::new(cli.network_name, cli.network_secret)); + cfg.set_network_identity(NetworkIdentity::new( + cli.network_name.clone(), + cli.network_secret.clone(), + )); cfg.set_dhcp(cli.dhcp); if let Some(ipv4) = &cli.ipv4 { - cfg.set_ipv4(Some( - ipv4.parse() - .with_context(|| format!("failed to parse ipv4 address: {}", ipv4)) - .unwrap(), - )) + cfg.set_ipv4(Some(ipv4.parse().with_context(|| { + format!("failed to parse ipv4 address: {}", ipv4) + })?)) } - cfg.set_peers( - cli.peers - .iter() - .map(|s| PeerConfig { - uri: s - .parse() - .with_context(|| format!("failed to parse peer uri: {}", s)) - .unwrap(), - }) - .collect(), - ); + let mut peers = Vec::::with_capacity(cli.peers.len()); + for p in &cli.peers { + peers.push(PeerConfig { + uri: p + .parse() + .with_context(|| format!("failed to parse peer uri: {}", p))?, + }); + } + cfg.set_peers(peers); cfg.set_listeners( - Cli::parse_listeners(cli.no_listener, cli.listeners) + Cli::parse_listeners(cli.no_listener, cli.listeners.clone())? .into_iter() .map(|s| s.parse().unwrap()) .collect(), @@ -414,29 +414,28 @@ impl From for TomlConfigLoader { for n in cli.proxy_networks.iter() { cfg.add_proxy_cidr( n.parse() - .with_context(|| format!("failed to parse proxy network: {}", n)) - .unwrap(), + .with_context(|| format!("failed to parse proxy network: {}", n))?, ); } - cfg.set_rpc_portal(Cli::parse_rpc_portal(cli.rpc_portal)); + cfg.set_rpc_portal( + Cli::parse_rpc_portal(cli.rpc_portal.clone()) + .with_context(|| format!("failed to parse rpc portal: {}", cli.rpc_portal))?, + ); - if let Some(external_nodes) = cli.external_node { + if let Some(external_nodes) = cli.external_node.as_ref() { let mut old_peers = cfg.get_peers(); old_peers.push(PeerConfig { - uri: external_nodes - .parse() - .with_context(|| { - format!("failed to parse external node uri: {}", external_nodes) - }) - .unwrap(), + uri: external_nodes.parse().with_context(|| { + format!("failed to parse external node uri: {}", external_nodes) + })?, }); cfg.set_peers(old_peers); } if cli.console_log_level.is_some() { cfg.set_console_logger_config(ConsoleLoggerConfig { - level: cli.console_log_level, + level: cli.console_log_level.clone(), }); } @@ -448,43 +447,37 @@ impl From for TomlConfigLoader { }); } - cfg.set_inst_name(cli.instance_name); + cfg.set_inst_name(cli.instance_name.clone()); - if let Some(vpn_portal) = cli.vpn_portal { + if let Some(vpn_portal) = cli.vpn_portal.as_ref() { let url: url::Url = vpn_portal .parse() - .with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal)) - .unwrap(); + .with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal))?; + let host = url + .host_str() + .ok_or_else(|| anyhow::anyhow!("vpn portal url missing host"))?; + let port = url + .port() + .ok_or_else(|| anyhow::anyhow!("vpn portal url missing port"))?; + let client_cidr = url.path()[1..].parse().with_context(|| { + format!("failed to parse vpn portal client cidr: {}", url.path()) + })?; + let wireguard_listen: SocketAddr = format!("{}:{}", host, port).parse().unwrap(); cfg.set_vpn_portal_config(VpnPortalConfig { - client_cidr: url.path()[1..] - .parse() - .with_context(|| { - format!("failed to parse vpn portal client cidr: {}", url.path()) - }) - .unwrap(), - wireguard_listen: format!("{}:{}", url.host_str().unwrap(), url.port().unwrap()) - .parse() - .with_context(|| { - format!( - "failed to parse vpn portal wireguard listen address: {}", - url.host_str().unwrap() - ) - }) - .unwrap(), + wireguard_listen, + client_cidr, }); } - if let Some(manual_routes) = cli.manual_routes { - cfg.set_routes(Some( - manual_routes - .iter() - .map(|s| { - s.parse() - .with_context(|| format!("failed to parse route: {}", s)) - .unwrap() - }) - .collect(), - )); + if let Some(manual_routes) = cli.manual_routes.as_ref() { + let mut routes = Vec::::with_capacity(manual_routes.len()); + for r in manual_routes { + routes.push( + r.parse() + .with_context(|| format!("failed to parse route: {}", r))?, + ); + } + cfg.set_routes(Some(routes)); } #[cfg(feature = "socks5")] @@ -503,30 +496,29 @@ impl From for TomlConfigLoader { f.enable_encryption = !cli.disable_encryption; f.enable_ipv6 = !cli.disable_ipv6; f.latency_first = cli.latency_first; - f.dev_name = cli.dev_name.unwrap_or_default(); + f.dev_name = cli.dev_name.clone().unwrap_or_default(); if let Some(mtu) = cli.mtu { f.mtu = mtu as u32; } f.enable_exit_node = cli.enable_exit_node; f.no_tun = cli.no_tun || cfg!(not(feature = "tun")); f.use_smoltcp = cli.use_smoltcp; - if let Some(wl) = cli.relay_network_whitelist { + if let Some(wl) = cli.relay_network_whitelist.as_ref() { f.foreign_network_whitelist = wl.join(" "); } f.disable_p2p = cli.disable_p2p; f.relay_all_peer_rpc = cli.relay_all_peer_rpc; - if let Some(ipv6_listener) = cli.ipv6_listener { + if let Some(ipv6_listener) = cli.ipv6_listener.as_ref() { f.ipv6_listener = ipv6_listener .parse() - .with_context(|| format!("failed to parse ipv6 listener: {}", ipv6_listener)) - .unwrap(); + .with_context(|| format!("failed to parse ipv6 listener: {}", ipv6_listener))? } f.multi_thread = cli.multi_thread; cfg.set_flags(f); cfg.set_exit_nodes(cli.exit_nodes.clone()); - cfg + Ok(cfg) } } @@ -652,44 +644,69 @@ pub fn handle_event(mut events: EventBusSubscriber) -> tokio::task::JoinHandle<( }) } +#[cfg(target_os = "windows")] +fn win_service_set_work_dir(service_name: &std::ffi::OsString) -> anyhow::Result<()> { + use easytier::common::constants::WIN_SERVICE_WORK_DIR_REG_KEY; + use winreg::enums::*; + use winreg::RegKey; + + let hklm = RegKey::predef(HKEY_LOCAL_MACHINE); + let key = hklm.open_subkey_with_flags(WIN_SERVICE_WORK_DIR_REG_KEY, KEY_READ)?; + let dir_pat_str = key.get_value::(service_name)?; + let dir_path = std::fs::canonicalize(dir_pat_str)?; + + std::env::set_current_dir(dir_path)?; + + Ok(()) +} + #[cfg(target_os = "windows")] fn win_service_event_loop( stop_notify: std::sync::Arc, - inst: launcher::NetworkInstance, + cli: Cli, status_handle: windows_service::service_control_handler::ServiceStatusHandle, ) { use std::time::Duration; use tokio::runtime::Runtime; use windows_service::service::*; + let normal_status = ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Running, + controls_accepted: ServiceControlAccept::STOP, + exit_code: ServiceExitCode::Win32(0), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }; + let error_status = ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + exit_code: ServiceExitCode::ServiceSpecific(1u32), + checkpoint: 0, + wait_hint: Duration::default(), + process_id: None, + }; + std::thread::spawn(move || { let rt = Runtime::new().unwrap(); rt.block_on(async move { tokio::select! { - res = inst.wait() => { - if let Some(e) = res { - status_handle.set_service_status(ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Stopped, - controls_accepted: ServiceControlAccept::empty(), - checkpoint: 0, - wait_hint: Duration::default(), - exit_code: ServiceExitCode::ServiceSpecific(1u32), - process_id: None - }).unwrap(); - panic!("launcher error: {:?}", e); + res = run_main(cli) => { + match res { + Ok(_) => { + status_handle.set_service_status(normal_status).unwrap(); + std::process::exit(0); + } + Err(e) => { + status_handle.set_service_status(error_status).unwrap(); + eprintln!("error: {}", e); + } } }, _ = stop_notify.notified() => { - status_handle.set_service_status(ServiceStatus { - service_type: ServiceType::OWN_PROCESS, - current_state: ServiceState::Stopped, - controls_accepted: ServiceControlAccept::empty(), - checkpoint: 0, - wait_hint: Duration::default(), - exit_code: ServiceExitCode::Win32(0), - process_id: None - }).unwrap(); + _ = status_handle.set_service_status(normal_status); std::process::exit(0); } } @@ -698,17 +715,16 @@ fn win_service_event_loop( } #[cfg(target_os = "windows")] -fn win_service_main(_: Vec) { +fn win_service_main(arg: Vec) { use std::sync::Arc; use std::time::Duration; use tokio::sync::Notify; use windows_service::service::*; use windows_service::service_control_handler::*; - let cli = Cli::parse(); - let cfg = TomlConfigLoader::from(cli); + _ = win_service_set_work_dir(&arg[0]); - init_logger(&cfg, false).unwrap(); + let cli = Cli::parse(); let stop_notify_send = Arc::new(Notify::new()); let stop_notify_recv = Arc::clone(&stop_notify_send); @@ -732,39 +748,16 @@ fn win_service_main(_: Vec) { wait_hint: Duration::default(), process_id: None, }; - let mut inst = launcher::NetworkInstance::new(cfg).set_fetch_node_info(false); - - inst.start().unwrap(); status_handle .set_service_status(next_status) .expect("set service status fail"); - win_service_event_loop(stop_notify_recv, inst, status_handle); + + win_service_event_loop(stop_notify_recv, cli, status_handle); } -#[tokio::main] -async fn main() { - let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); - rust_i18n::set_locale(&locale); - - #[cfg(target_os = "windows")] - match windows_service::service_dispatcher::start(String::new(), ffi_service_main) { - Ok(_) => std::thread::park(), - Err(e) => { - let should_panic = if let windows_service::Error::Winapi(ref io_error) = e { - io_error.raw_os_error() != Some(0x427) // ERROR_FAILED_SERVICE_CONTROLLER_CONNECT - } else { - true - }; - - if should_panic { - panic!("SCM start an error: {}", e); - } - } - }; - - let cli = Cli::parse(); - - setup_panic_handler(); +async fn run_main(cli: Cli) -> anyhow::Result<()> { + let cfg = TomlConfigLoader::try_from(&cli)?; + init_logger(&cfg, false)?; if cli.config_server.is_some() { let config_server_url_s = cli.config_server.clone().unwrap(); @@ -797,12 +790,9 @@ async fn main() { let _wc = web_client::WebClient::new(UdpTunnelConnector::new(c_url), token.to_string()); tokio::signal::ctrl_c().await.unwrap(); - return; + return Ok(()); } - let cfg = TomlConfigLoader::from(cli); - init_logger(&cfg, false).unwrap(); - println!("Starting easytier with config:"); println!("############### TOML ###############\n"); println!("{}", cfg.dump()); @@ -811,6 +801,37 @@ async fn main() { let mut l = launcher::NetworkInstance::new(cfg).set_fetch_node_info(false); let _t = ScopedTask::from(handle_event(l.start().unwrap())); if let Some(e) = l.wait().await { - panic!("launcher error: {:?}", e); + anyhow::bail!("launcher error: {}", e); + } + Ok(()) +} + +#[tokio::main] +async fn main() { + let locale = sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")); + rust_i18n::set_locale(&locale); + setup_panic_handler(); + + #[cfg(target_os = "windows")] + match windows_service::service_dispatcher::start(String::new(), ffi_service_main) { + Ok(_) => std::thread::park(), + Err(e) => { + let should_panic = if let windows_service::Error::Winapi(ref io_error) = e { + io_error.raw_os_error() != Some(0x427) // ERROR_FAILED_SERVICE_CONTROLLER_CONNECT + } else { + true + }; + + if should_panic { + panic!("SCM start an error: {}", e); + } + } + }; + + let cli = Cli::parse(); + + if let Err(e) = run_main(cli).await { + eprintln!("error: {:?}", e); + std::process::exit(1); } } diff --git a/easytier/src/utils.rs b/easytier/src/utils.rs index 5160f04..b62a241 100644 --- a/easytier/src/utils.rs +++ b/easytier/src/utils.rs @@ -168,4 +168,4 @@ mod tests { tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; tracing::debug!("test display debug"); } -} +} \ No newline at end of file