service.rs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
  1. use crate::config::{Config, IVerge};
  2. use crate::core::handle;
  3. use crate::utils::dirs;
  4. use anyhow::{bail, Context, Result};
  5. use serde::{Deserialize, Serialize};
  6. use std::collections::HashMap;
  7. use std::path::PathBuf;
  8. use std::time::Duration;
  9. use std::{env::current_exe, process::Command as StdCommand};
  10. use tokio::time::sleep;
  11. // Windows only
  12. const SERVICE_URL: &str = "http://127.0.0.1:33211";
  13. #[derive(Debug, Deserialize, Serialize, Clone)]
  14. pub struct ResponseBody {
  15. pub core_type: Option<String>,
  16. pub bin_path: String,
  17. pub config_dir: String,
  18. pub log_file: String,
  19. }
  20. #[derive(Debug, Deserialize, Serialize, Clone)]
  21. pub struct JsonResponse {
  22. pub code: u64,
  23. pub msg: String,
  24. pub data: Option<ResponseBody>,
  25. }
  26. #[cfg(not(target_os = "windows"))]
  27. pub fn sudo(passwd: &String, cmd: String) -> StdCommand {
  28. let shell = format!("echo {} | sudo -S {}", passwd, cmd);
  29. let mut command = StdCommand::new("bash");
  30. command.arg("-c").arg(shell);
  31. command
  32. }
  33. /// Install the Clash Verge Service
  34. /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
  35. ///
  36. #[cfg(target_os = "windows")]
  37. pub async fn install_service(_passwd: String) -> Result<()> {
  38. use deelevate::{PrivilegeLevel, Token};
  39. use runas::Command as RunasCommand;
  40. use std::os::windows::process::CommandExt;
  41. let binary_path = dirs::service_path()?;
  42. let install_path = binary_path.with_file_name("install-service.exe");
  43. if !install_path.exists() {
  44. bail!("installer exe not found");
  45. }
  46. let token = Token::with_current_process()?;
  47. let level = token.privilege_level()?;
  48. let status = match level {
  49. PrivilegeLevel::NotPrivileged => RunasCommand::new(install_path).show(false).status()?,
  50. _ => StdCommand::new(install_path)
  51. .creation_flags(0x08000000)
  52. .status()?,
  53. };
  54. if !status.success() {
  55. bail!(
  56. "failed to install service with status {}",
  57. status.code().unwrap()
  58. );
  59. }
  60. Ok(())
  61. }
  62. #[cfg(target_os = "linux")]
  63. pub async fn install_service(passwd: String) -> Result<()> {
  64. use users::get_effective_uid;
  65. let binary_path = dirs::service_path()?;
  66. let installer_path = binary_path.with_file_name("install-service");
  67. if !installer_path.exists() {
  68. bail!("installer not found");
  69. }
  70. let output = match get_effective_uid() {
  71. 0 => {
  72. StdCommand::new("chmod")
  73. .arg("+x")
  74. .arg(installer_path.clone())
  75. .output()?;
  76. StdCommand::new("chmod")
  77. .arg("+x")
  78. .arg(binary_path)
  79. .output()?;
  80. StdCommand::new(installer_path.clone()).output()?
  81. }
  82. _ => {
  83. sudo(
  84. &passwd,
  85. format!("chmod +x {}", installer_path.to_string_lossy()),
  86. )
  87. .output()?;
  88. sudo(
  89. &passwd,
  90. format!("chmod +x {}", binary_path.to_string_lossy()),
  91. )
  92. .output()?;
  93. sudo(&passwd, format!("{}", installer_path.to_string_lossy())).output()?
  94. }
  95. };
  96. if output.stderr.len() > 0 {
  97. bail!(
  98. "failed to install service with error: {}",
  99. String::from_utf8_lossy(&output.stderr)
  100. );
  101. }
  102. Ok(())
  103. }
  104. #[cfg(target_os = "macos")]
  105. pub async fn install_service(passwd: String) -> Result<()> {
  106. let binary_path = dirs::service_path()?;
  107. let installer_path = binary_path.with_file_name("install-service");
  108. if !installer_path.exists() {
  109. bail!("installer not found");
  110. }
  111. sudo(
  112. &passwd,
  113. format!(
  114. "chmod +x {}",
  115. installer_path.to_string_lossy().replace(" ", "\\ ")
  116. ),
  117. )
  118. .output()?;
  119. let output = sudo(
  120. &passwd,
  121. format!("{}", installer_path.to_string_lossy().replace(" ", "\\ ")),
  122. )
  123. .output()?;
  124. if output.stderr.len() > 0 {
  125. bail!(
  126. "failed to install service with error: {}",
  127. String::from_utf8_lossy(&output.stderr)
  128. );
  129. }
  130. Ok(())
  131. }
  132. /// Uninstall the Clash Verge Service
  133. /// 该函数应该在协程或者线程中执行,避免UAC弹窗阻塞主线程
  134. #[cfg(target_os = "windows")]
  135. pub async fn uninstall_service(_passwd: String) -> Result<()> {
  136. use deelevate::{PrivilegeLevel, Token};
  137. use runas::Command as RunasCommand;
  138. use std::os::windows::process::CommandExt;
  139. let binary_path = dirs::service_path()?;
  140. let uninstall_path = binary_path.with_file_name("uninstall-service.exe");
  141. if !uninstall_path.exists() {
  142. bail!("uninstaller exe not found");
  143. }
  144. let token = Token::with_current_process()?;
  145. let level = token.privilege_level()?;
  146. let status = match level {
  147. PrivilegeLevel::NotPrivileged => RunasCommand::new(uninstall_path).show(false).status()?,
  148. _ => StdCommand::new(uninstall_path)
  149. .creation_flags(0x08000000)
  150. .status()?,
  151. };
  152. if !status.success() {
  153. bail!(
  154. "failed to uninstall service with status {}",
  155. status.code().unwrap()
  156. );
  157. }
  158. Ok(())
  159. }
  160. #[cfg(target_os = "linux")]
  161. pub async fn uninstall_service(passwd: String) -> Result<()> {
  162. use users::get_effective_uid;
  163. let binary_path = dirs::service_path()?;
  164. let uninstaller_path = binary_path.with_file_name("uninstall-service");
  165. if !uninstaller_path.exists() {
  166. bail!("uninstaller not found");
  167. }
  168. let output = match get_effective_uid() {
  169. 0 => {
  170. StdCommand::new("chmod")
  171. .arg("+x")
  172. .arg(uninstaller_path.clone())
  173. .output()?;
  174. StdCommand::new(uninstaller_path.clone()).output()?
  175. }
  176. _ => {
  177. sudo(
  178. &passwd,
  179. format!("chmod +x {}", uninstaller_path.to_string_lossy()),
  180. )
  181. .output()?;
  182. sudo(&passwd, format!("{}", uninstaller_path.to_string_lossy())).output()?
  183. }
  184. };
  185. if output.stderr.len() > 0 {
  186. bail!(
  187. "failed to install service with error: {}",
  188. String::from_utf8_lossy(&output.stderr)
  189. );
  190. }
  191. Ok(())
  192. }
  193. #[cfg(target_os = "macos")]
  194. pub async fn uninstall_service(passwd: String) -> Result<()> {
  195. let binary_path = dirs::service_path()?;
  196. let uninstaller_path = binary_path.with_file_name("uninstall-service");
  197. if !uninstaller_path.exists() {
  198. bail!("uninstaller not found");
  199. }
  200. sudo(
  201. &passwd,
  202. format!(
  203. "chmod +x {}",
  204. uninstaller_path.to_string_lossy().replace(" ", "\\ ")
  205. ),
  206. )
  207. .output()?;
  208. let output = sudo(
  209. &passwd,
  210. format!("{}", uninstaller_path.to_string_lossy().replace(" ", "\\ ")),
  211. )
  212. .output()?;
  213. if output.stderr.len() > 0 {
  214. bail!(
  215. "failed to uninstall service with error: {}",
  216. String::from_utf8_lossy(&output.stderr)
  217. );
  218. }
  219. Ok(())
  220. }
  221. /// check the windows service status
  222. pub async fn check_service() -> Result<JsonResponse> {
  223. let url = format!("{SERVICE_URL}/get_clash");
  224. let response = reqwest::ClientBuilder::new()
  225. .no_proxy()
  226. .build()?
  227. .get(url)
  228. .send()
  229. .await
  230. .context("failed to connect to the Clash Verge Service")?
  231. .json::<JsonResponse>()
  232. .await
  233. .context("failed to parse the Clash Verge Service response")?;
  234. Ok(response)
  235. }
  236. /// start the clash by service
  237. pub(super) async fn run_core_by_service(config_file: &PathBuf) -> Result<()> {
  238. let status = check_service().await?;
  239. if status.code == 0 {
  240. stop_core_by_service().await?;
  241. sleep(Duration::from_secs(1)).await;
  242. }
  243. let clash_core = { Config::verge().latest().clash_core.clone() };
  244. let mut clash_core = clash_core.unwrap_or("verge-mihomo".into());
  245. // compatibility
  246. if clash_core.contains("clash") {
  247. clash_core = "verge-mihomo".to_string();
  248. Config::verge().draft().patch_config(IVerge {
  249. clash_core: Some("verge-mihomo".to_string()),
  250. ..IVerge::default()
  251. });
  252. Config::verge().apply();
  253. match Config::verge().data().save_file() {
  254. Ok(_) => handle::Handle::refresh_verge(),
  255. Err(err) => log::error!(target: "app", "{err}"),
  256. }
  257. }
  258. let bin_ext = if cfg!(windows) { ".exe" } else { "" };
  259. let clash_bin = format!("{clash_core}{bin_ext}");
  260. let bin_path = current_exe()?.with_file_name(clash_bin);
  261. let bin_path = dirs::path_to_str(&bin_path)?;
  262. let config_dir = dirs::app_home_dir()?;
  263. let config_dir = dirs::path_to_str(&config_dir)?;
  264. let log_path = dirs::service_log_file()?;
  265. let log_path = dirs::path_to_str(&log_path)?;
  266. let config_file = dirs::path_to_str(config_file)?;
  267. let mut map = HashMap::new();
  268. map.insert("core_type", clash_core.as_str());
  269. map.insert("bin_path", bin_path);
  270. map.insert("config_dir", config_dir);
  271. map.insert("config_file", config_file);
  272. map.insert("log_file", log_path);
  273. let url = format!("{SERVICE_URL}/start_clash");
  274. let res = reqwest::ClientBuilder::new()
  275. .no_proxy()
  276. .build()?
  277. .post(url)
  278. .json(&map)
  279. .send()
  280. .await?
  281. .json::<JsonResponse>()
  282. .await
  283. .context("failed to connect to the Clash Verge Service")?;
  284. if res.code != 0 {
  285. bail!(res.msg);
  286. }
  287. Ok(())
  288. }
  289. /// stop the clash by service
  290. pub(super) async fn stop_core_by_service() -> Result<()> {
  291. let url = format!("{SERVICE_URL}/stop_clash");
  292. let res = reqwest::ClientBuilder::new()
  293. .no_proxy()
  294. .build()?
  295. .post(url)
  296. .send()
  297. .await?
  298. .json::<JsonResponse>()
  299. .await
  300. .context("failed to connect to the Clash Verge Service")?;
  301. if res.code != 0 {
  302. bail!(res.msg);
  303. }
  304. Ok(())
  305. }
  306. /// set dns by service
  307. pub async fn set_dns_by_service() -> Result<()> {
  308. let url = format!("{SERVICE_URL}/set_dns");
  309. let res = reqwest::ClientBuilder::new()
  310. .no_proxy()
  311. .build()?
  312. .post(url)
  313. .send()
  314. .await?
  315. .json::<JsonResponse>()
  316. .await
  317. .context("failed to connect to the Clash Verge Service")?;
  318. if res.code != 0 {
  319. bail!(res.msg);
  320. }
  321. Ok(())
  322. }
  323. /// unset dns by service
  324. pub async fn unset_dns_by_service() -> Result<()> {
  325. let url = format!("{SERVICE_URL}/unset_dns");
  326. let res = reqwest::ClientBuilder::new()
  327. .no_proxy()
  328. .build()?
  329. .post(url)
  330. .send()
  331. .await?
  332. .json::<JsonResponse>()
  333. .await
  334. .context("failed to connect to the Clash Verge Service")?;
  335. if res.code != 0 {
  336. bail!(res.msg);
  337. }
  338. Ok(())
  339. }