From 084f0dedddd732edf1c7af9be27892a20463b3d5 Mon Sep 17 00:00:00 2001 From: chao wan <1013448513@qq.com> Date: Sat, 2 Nov 2024 23:37:21 +0800 Subject: [PATCH] Support service manager --- Cargo.lock | 41 ++++ easytier/Cargo.toml | 2 + easytier/locales/app.yml | 9 + easytier/src/arch/windows.rs | 151 ++++++++++++++- easytier/src/easytier-core.rs | 353 +++++++++++++++++++++++----------- easytier/src/utils.rs | 71 +++++++ 6 files changed, 518 insertions(+), 109 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e885bd2..d38f58c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1041,6 +1041,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" @@ -1667,6 +1676,7 @@ dependencies = [ "serde", "serde_json", "serial_test", + "service-manager", "smoltcp", "socket2", "stun_codec", @@ -1845,6 +1855,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" @@ -5559,6 +5580,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" @@ -7869,6 +7904,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 = "yasna" version = "0.5.2" 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..8da6467 100644 --- a/easytier/locales/app.yml +++ b/easytier/locales/app.yml @@ -120,6 +120,15 @@ 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端口上监听" + work_dir: + en: "specify the working directory for the program" + zh-CN: "指定程序的工作目录" + run: + en: "Run the EasyTier core application" + zh-CN: "运行 EasyTier 核心应用程序" + service: + en: "Manage the EasyTier service" + zh-CN: "管理 EasyTier 服务" core_app: panic_backtrace_save: diff --git a/easytier/src/arch/windows.rs b/easytier/src/arch/windows.rs index 2ee8452..497b5ad 100644 --- a/easytier/src/arch/windows.rs +++ b/easytier/src/arch/windows.rs @@ -1,5 +1,5 @@ use std::{ - ffi::c_void, + ffi::{c_void, OsStr, OsString}, io::{self, ErrorKind}, mem, net::SocketAddr, @@ -11,13 +11,34 @@ use network_interface::NetworkInterfaceConfig; use windows_sys::{ core::PCSTR, Win32::{ - Foundation::{BOOL, FALSE}, + Foundation::{BOOL, FALSE, ERROR_SERVICE_DOES_NOT_EXIST}, Networking::WinSock::{ htonl, setsockopt, WSAGetLastError, WSAIoctl, IPPROTO_IP, IPPROTO_IPV6, IPV6_UNICAST_IF, IP_UNICAST_IF, SIO_UDP_CONNRESET, SOCKET, SOCKET_ERROR, }, }, }; +use service_manager::{ + ServiceInstallCtx, + ServiceLevel, + ServiceStartCtx, + ServiceStatus, + ServiceStatusCtx, + ServiceUninstallCtx, + ServiceStopCtx +}; +use windows_service::service::{ + ServiceType, + ServiceErrorControl, + ServiceDependency, + ServiceInfo, + ServiceStartType, + ServiceAccess +}; +use windows_service::service_manager::{ + ServiceManagerAccess, + ServiceManager +}; pub fn disable_connection_reset(socket: &S) -> io::Result<()> { let handle = socket.as_raw_socket() as SOCKET; @@ -153,3 +174,129 @@ pub fn setup_socket_for_win( Ok(()) } + +pub struct WinServiceManager { + service_manager: ServiceManager, + display_name: Option, + description: Option, + dependencies: Vec +} + +impl WinServiceManager { + pub fn new(display_name: Option, description: Option, dependencies: Vec,) -> Result { + let service_manager = ServiceManager::local_computer( + None::<&str>, + ServiceManagerAccess::ALL_ACCESS, + )?; + Ok(Self { + service_manager, + display_name, + description, + dependencies, + }) + } +} + +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 dis_name = self.display_name.clone().unwrap_or_else(|| srv_name.clone()); + let dependencies = self.dependencies.iter().map(|dep| ServiceDependency::Service(dep.clone())).collect::>(); + 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) = &self.description { + service.set_description(s.clone()).map_err(|e| { + io::Error::new(io::ErrorKind::Other, e) + })?; + } + + 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(ERROR_SERVICE_DOES_NOT_EXIST as i32) { + 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), + } + } +} + diff --git a/easytier/src/easytier-core.rs b/easytier/src/easytier-core.rs index 0c22229..093d6a5 100644 --- a/easytier/src/easytier-core.rs +++ b/easytier/src/easytier-core.rs @@ -6,10 +6,16 @@ extern crate rust_i18n; use std::{ net::{Ipv4Addr, SocketAddr}, path::PathBuf, + ffi::OsString }; use anyhow::Context; -use clap::Parser; +use clap::{ + command, + Parser, + Subcommand, + Args +}; use tokio::net::TcpSocket; use easytier::{ @@ -24,7 +30,7 @@ use easytier::{ }, launcher, proto, tunnel::udp::UdpTunnelConnector, - utils::{init_logger, setup_panic_handler}, + utils::{init_logger, setup_panic_handler, Service}, web_client, }; @@ -39,15 +45,14 @@ use mimalloc_rust::GlobalMiMalloc; static GLOBAL_MIMALLOC: GlobalMiMalloc = GlobalMiMalloc; #[derive(Parser, Debug)] -#[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)] -struct Cli { +struct RunArgs { #[arg( short = 'w', long, help = t!("core_clap.config_server").to_string() )] - config_server: Option, - + config_server: Option, + #[arg( short, long, @@ -289,11 +294,47 @@ struct Cli { help = t!("core_clap.ipv6_listener").to_string() )] ipv6_listener: Option, + + #[arg( + long, + help = t!("core_clap.work_dir").to_string() + )] + work_dir: Option +} + +#[derive(Parser, Debug)] +#[command(name = "easytier-core", author, version = EASYTIER_VERSION , about, long_about = None)] +struct Cli{ + #[command(subcommand)] + sub_command: SubCmd +} +#[derive(Subcommand, Debug)] +enum SubCmd { + #[command( + about = t!("core_clap.run").to_string() + )] + Run(RunArgs), + #[command( + about = t!("core_clap.service").to_string() + )] + Service(ServiceArgs) +} + +#[derive(Args, Debug)] +struct ServiceArgs{ + #[command(subcommand)] + sub_command: SrvSubCmd +} +#[derive(Subcommand, Debug)] +enum SrvSubCmd { + Install(RunArgs), + Uninstall, + Status } rust_i18n::i18n!("locales", fallback = "en"); -impl Cli { +impl RunArgs { fn parse_listeners(no_listener: bool, listeners: Vec) -> Vec { let proto_port_offset = vec![("tcp", 0), ("udp", 0), ("wg", 1), ("ws", 1), ("wss", 2)]; @@ -351,7 +392,7 @@ impl Cli { if port == 0 { // check tcp 15888 first for i in 15888..15900 { - if let Some(s) = Cli::check_tcp_available(i) { + if let Some(s) = RunArgs::check_tcp_available(i) { return s; } } @@ -364,27 +405,27 @@ impl Cli { } } -impl From for TomlConfigLoader { - fn from(cli: Cli) -> Self { - if let Some(config_file) = &cli.config_file { +impl From for TomlConfigLoader { + fn from(args: RunArgs) -> Self { + if let Some(config_file) = &args.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)) + .with_context(|| format!("failed to load config file: {:?}", args.config_file)) .unwrap(); } let cfg = TomlConfigLoader::default(); - cfg.set_hostname(cli.hostname); + cfg.set_hostname(args.hostname); - cfg.set_network_identity(NetworkIdentity::new(cli.network_name, cli.network_secret)); + cfg.set_network_identity(NetworkIdentity::new(args.network_name, args.network_secret)); - cfg.set_dhcp(cli.dhcp); + cfg.set_dhcp(args.dhcp); - if let Some(ipv4) = &cli.ipv4 { + if let Some(ipv4) = &args.ipv4 { cfg.set_ipv4(Some( ipv4.parse() .with_context(|| format!("failed to parse ipv4 address: {}", ipv4)) @@ -393,7 +434,7 @@ impl From for TomlConfigLoader { } cfg.set_peers( - cli.peers + args.peers .iter() .map(|s| PeerConfig { uri: s @@ -405,13 +446,13 @@ impl From for TomlConfigLoader { ); cfg.set_listeners( - Cli::parse_listeners(cli.no_listener, cli.listeners) + RunArgs::parse_listeners(args.no_listener, args.listeners) .into_iter() .map(|s| s.parse().unwrap()) .collect(), ); - for n in cli.proxy_networks.iter() { + for n in args.proxy_networks.iter() { cfg.add_proxy_cidr( n.parse() .with_context(|| format!("failed to parse proxy network: {}", n)) @@ -419,9 +460,9 @@ impl From for TomlConfigLoader { ); } - cfg.set_rpc_portal(Cli::parse_rpc_portal(cli.rpc_portal)); + cfg.set_rpc_portal(RunArgs::parse_rpc_portal(args.rpc_portal)); - if let Some(external_nodes) = cli.external_node { + if let Some(external_nodes) = args.external_node { let mut old_peers = cfg.get_peers(); old_peers.push(PeerConfig { uri: external_nodes @@ -434,23 +475,23 @@ impl From for TomlConfigLoader { cfg.set_peers(old_peers); } - if cli.console_log_level.is_some() { + if args.console_log_level.is_some() { cfg.set_console_logger_config(ConsoleLoggerConfig { - level: cli.console_log_level, + level: args.console_log_level, }); } - if cli.file_log_dir.is_some() || cli.file_log_level.is_some() { + if args.file_log_dir.is_some() || args.file_log_level.is_some() { cfg.set_file_logger_config(FileLoggerConfig { - level: cli.file_log_level.clone(), - dir: cli.file_log_dir.clone(), - file: Some(format!("easytier-{}", cli.instance_name)), + level: args.file_log_level.clone(), + dir: args.file_log_dir.clone(), + file: Some(format!("easytier-{}", args.instance_name)), }); } - cfg.set_inst_name(cli.instance_name); + cfg.set_inst_name(args.instance_name); - if let Some(vpn_portal) = cli.vpn_portal { + if let Some(vpn_portal) = args.vpn_portal { let url: url::Url = vpn_portal .parse() .with_context(|| format!("failed to parse vpn portal url: {}", vpn_portal)) @@ -474,7 +515,7 @@ impl From for TomlConfigLoader { }); } - if let Some(manual_routes) = cli.manual_routes { + if let Some(manual_routes) = args.manual_routes { cfg.set_routes(Some( manual_routes .iter() @@ -488,7 +529,7 @@ impl From for TomlConfigLoader { } #[cfg(feature = "socks5")] - if let Some(socks5_proxy) = cli.socks5 { + if let Some(socks5_proxy) = args.socks5 { cfg.set_socks5_portal(Some( format!("socks5://0.0.0.0:{}", socks5_proxy) .parse() @@ -497,34 +538,34 @@ impl From for TomlConfigLoader { } let mut f = cfg.get_flags(); - if cli.default_protocol.is_some() { - f.default_protocol = cli.default_protocol.as_ref().unwrap().clone(); + if args.default_protocol.is_some() { + f.default_protocol = args.default_protocol.as_ref().unwrap().clone(); } - 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(); - if let Some(mtu) = cli.mtu { + f.enable_encryption = !args.disable_encryption; + f.enable_ipv6 = !args.disable_ipv6; + f.latency_first = args.latency_first; + f.dev_name = args.dev_name.unwrap_or_default(); + if let Some(mtu) = args.mtu { f.mtu = mtu; } - 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 { + f.enable_exit_node = args.enable_exit_node; + f.no_tun = args.no_tun || cfg!(not(feature = "tun")); + f.use_smoltcp = args.use_smoltcp; + if let Some(wl) = args.relay_network_whitelist { 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 { + f.disable_p2p = args.disable_p2p; + f.relay_all_peer_rpc = args.relay_all_peer_rpc; + if let Some(ipv6_listener) = args.ipv6_listener { f.ipv6_listener = ipv6_listener .parse() .with_context(|| format!("failed to parse ipv6 listener: {}", ipv6_listener)) .unwrap(); } - f.multi_thread = cli.multi_thread; + f.multi_thread = args.multi_thread; cfg.set_flags(f); - cfg.set_exit_nodes(cli.exit_nodes.clone()); + cfg.set_exit_nodes(args.exit_nodes.clone()); cfg } @@ -654,42 +695,48 @@ pub fn handle_event(mut events: EventBusSubscriber) -> tokio::task::JoinHandle<( #[cfg(target_os = "windows")] fn win_service_event_loop( + args: RunArgs, stop_notify: std::sync::Arc, - inst: launcher::NetworkInstance, - status_handle: windows_service::service_control_handler::ServiceStatusHandle, + status_handle: windows_service::service_control_handler::ServiceStatusHandle, ) { use tokio::runtime::Runtime; use std::time::Duration; use windows_service::service::*; + let err_stop = ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::empty(), + checkpoint: 0, + wait_hint: Duration::default(), + exit_code: ServiceExitCode::Win32(1), + process_id: None, + }; + let normal_stop = ServiceStatus { + service_type: ServiceType::OWN_PROCESS, + current_state: ServiceState::Stopped, + controls_accepted: ServiceControlAccept::STOP, + checkpoint: 0, + wait_hint: Duration::default(), + exit_code: ServiceExitCode::Win32(0), + 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 = main_run(args) => { + if let Err(e) = res { + status_handle.set_service_status(err_stop).unwrap(); + panic!("{:?}", e); + } else { + status_handle.set_service_status(normal_stop).unwrap(); + std::process::exit(0); } }, _ = 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_stop).unwrap(); std::process::exit(0); } } @@ -698,17 +745,20 @@ fn win_service_event_loop( } #[cfg(target_os = "windows")] -fn win_service_main(_: Vec) { +fn win_service_main(_: Vec) { use std::time::Duration; use windows_service::service_control_handler::*; use windows_service::service::*; use std::sync::Arc; use tokio::sync::Notify; - let cli = Cli::parse(); - let cfg = TomlConfigLoader::from(cli); - - init_logger(&cfg, false).unwrap(); + let args = RunArgs::try_parse().unwrap_or_else(|_| { + if let SubCmd::Run(args_) = Cli::parse().sub_command { + args_ + } else { + panic!("invalid args") + } + }); let stop_notify_send = Arc::new(Notify::new()); let stop_notify_recv = Arc::clone(&stop_notify_send); @@ -735,39 +785,81 @@ 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(args, stop_notify_recv, 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); +fn service_manager_handle(srv_arg: ServiceArgs) -> Result<(), String> { + use service_manager::ServiceStatus; + let service = Service::new().map_err(|e| { + format!("Service manager init failed: {:?}", e) + })?; + let status = service.status().map_err(|e|{ + format!("Failed to get service info: {:?}", e) + })?; - #[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(); + match srv_arg.sub_command { + SrvSubCmd::Install(_) => { + if status == ServiceStatus::NotInstalled { + let mut args = std::env::args_os().skip(3).collect::>(); + args.insert(0, OsString::from("run")); + + if let Some(work_dir) = args.iter().position(|x| x == "--work-dir") { + let d = std::fs::canonicalize(&args[work_dir + 1]).map_err(|e| { + format!("failed to get work dir: {:?}", e) + })?; + args[work_dir + 1] = OsString::from(d); + } else { + let d = std::env::current_exe().unwrap().parent().unwrap().to_path_buf(); + args.push(OsString::from("--work-dir")); + args.push(OsString::from(d)); + } + service.install(args).map_err(|e| { + format!("Service install failed: {:?}", e) + })?; + println!("Service installed successfully."); + } + else { + return Err("Service already installed, please uninstall it first.".to_string()); + } + } + SrvSubCmd::Uninstall => { + if status != ServiceStatus::NotInstalled { + if status == ServiceStatus::Running{ + service.stop().map_err(|e| { + format!("Service stop failed: {:?}", e) + })?; + } + service.uninstall().map_err(|e| { + format!("Service uninstall failed: {:?}", e) + })?; + println!("Service uninstalled successfully."); + } + else { + eprint!("Service not installed."); + } + } + SrvSubCmd::Status => { + println!("Service status: {}", match status { + ServiceStatus::NotInstalled => "Not Installed", + ServiceStatus::Stopped(_) => "Stopped", + ServiceStatus::Running => "Running", + }); + } + } + Ok(()) +} - setup_panic_handler(); - - if cli.config_server.is_some() { - let config_server_url_s = cli.config_server.clone().unwrap(); +async fn main_run(args: RunArgs) -> Result<(), String> { + if let Some(work_dir) = &args.work_dir { + std::env::set_current_dir(work_dir).map_err(|e| { + format!("failed to set work dir: {:?}", e) + })?; + } + + if args.config_server.is_some() { + let config_server_url_s = args.config_server.clone().unwrap(); let config_server_url = match url::Url::parse(&config_server_url_s) { Ok(u) => u, Err(_) => format!( @@ -797,10 +889,11 @@ 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); + let cfg = TomlConfigLoader::from(args); + init_logger(&cfg, false).unwrap(); println!("Starting easytier with config:"); @@ -810,7 +903,53 @@ 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); + if let Some(e) = l.wait().await{ + Err(format!("launcher error: {}", e)) + } else { + Ok(()) + } + +} + +#[tokio::main] +async fn main() { + setup_panic_handler(); + + 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 run_result = if let Ok(args) = RunArgs::try_parse() { + main_run(args).await + } else { + match Cli::parse().sub_command { + SubCmd::Run(args) => main_run(args).await, + SubCmd::Service(serv_args) => { + if let Err(e) = service_manager_handle(serv_args) { + eprint!("{}", e); + std::process::exit(1); + } + return; + } + } + }; + + if let Err(e) = run_result { + panic!("{:?}", e); } } + diff --git a/easytier/src/utils.rs b/easytier/src/utils.rs index 5160f04..67c1b4d 100644 --- a/easytier/src/utils.rs +++ b/easytier/src/utils.rs @@ -1,6 +1,8 @@ use anyhow::Context; use tracing::level_filters::LevelFilter; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; +use std::ffi::OsString; +use service_manager::*; use crate::common::{config::ConfigLoader, get_logger_timer_rfc3339}; @@ -153,6 +155,75 @@ pub fn setup_panic_handler() { })); } +pub struct Service{ + lable: ServiceLabel, + service_manager: Box +} + +impl Service { + pub fn new() -> Result { + let service_manager = if cfg!(windows) { + Box::new( + crate::arch::windows::WinServiceManager::new( + Some(OsString::from("EasyTier Service")), + Some(OsString::from(env!("CARGO_PKG_DESCRIPTION"))), + vec![OsString::from("dnscache"), OsString::from("rpcss")], + )? + ) + } else { + ::native()? + }; + Ok(Self { + lable: env!("CARGO_PKG_NAME").parse().unwrap(), + service_manager + }) + } + + pub fn install(&self, bin_args: Vec) -> Result<(), std::io::Error> { + let ctx = ServiceInstallCtx { + label: self.lable.clone(), + contents: None, + program: std::env::current_exe().unwrap(), + args: bin_args, + autostart: true, + username: None, + working_directory: None, + environment: None, + }; + self.service_manager.install(ctx)?; + Ok(()) + } + + pub fn uninstall(&self) -> Result<(), std::io::Error> { + let ctx = ServiceUninstallCtx { + label: self.lable.clone(), + }; + self.service_manager.uninstall(ctx) + } + + pub fn status(&self) -> Result { + let ctx = ServiceStatusCtx { + label: self.lable.clone(), + }; + self.service_manager.status(ctx) + } + + pub fn start(&self) -> Result<(), std::io::Error> { + let ctx = ServiceStartCtx { + label: self.lable.clone(), + }; + self.service_manager.start(ctx) + } + + pub fn stop(&self) -> Result<(), std::io::Error> { + let ctx = ServiceStopCtx { + label: self.lable.clone(), + }; + self.service_manager.stop(ctx) + } + +} + #[cfg(test)] mod tests { use crate::common::config::{self};