Refine ProxyWarden routing and config flows

This commit is contained in:
2026-07-10 21:15:17 +03:00
parent 9fd0a8c0b9
commit dbba3806cc
31 changed files with 1823 additions and 191 deletions
+671 -66
View File
File diff suppressed because it is too large Load Diff
+156 -26
View File
@@ -9,6 +9,10 @@ use std::{
path::{Path, PathBuf},
};
pub const PROXYWARDEN_COMPONENTS_DIR_NAME: &str = "components";
pub const PROXIFYRE_COMPONENT_DIR_NAME: &str = "ProxiFyre";
pub const SINGBOX_COMPONENT_DIR_NAME: &str = "sing-box";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ProxyfierEngine {
ProxiFyre,
@@ -23,6 +27,13 @@ pub struct DetectedProxyfier {
pub config_path: Option<PathBuf>,
pub running: bool,
pub service_name: Option<String>,
pub service_status: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectedService {
pub name: String,
pub status: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -50,7 +61,12 @@ pub trait ProxyfierDetectionHost {
fn process_running(&self, process_name: &str) -> bool;
fn service_running(&self, service_name: &str) -> bool;
fn service_status(&self, service_name: &str) -> Option<String>;
fn service_running(&self, service_name: &str) -> bool {
self.service_status(service_name)
.is_some_and(|status| service_status_is_running(&status))
}
fn registry_install_entries(&self) -> Vec<RegistryInstallEntry>;
}
@@ -77,13 +93,13 @@ impl ProxyfierDetectionHost for SystemProxyfierDetectionHost {
powershell_bool(&script)
}
fn service_running(&self, service_name: &str) -> bool {
fn service_status(&self, service_name: &str) -> Option<String> {
let script = format!(
"$s = Get-Service -Name '{}' -ErrorAction SilentlyContinue; if ($s -and $s.Status -eq 'Running') {{ 'true' }} else {{ 'false' }}",
"$s = Get-Service -Name '{}' -ErrorAction SilentlyContinue; if ($s) {{ $s.Status.ToString() }}",
escape_powershell_single(service_name)
);
powershell_bool(&script)
powershell_text(&script).map(|status| status.to_ascii_lowercase())
}
fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {
@@ -95,16 +111,53 @@ pub fn detect_proxyfier_install() -> Option<DetectedProxyfier> {
detect_proxyfier_install_with_host(&SystemProxyfierDetectionHost)
}
pub fn app_install_dir_from_current_exe() -> Option<PathBuf> {
env::current_exe()
.ok()
.and_then(|path| path.parent().map(Path::to_path_buf))
}
pub fn component_root_from_app_dir(app_dir: &Path) -> PathBuf {
app_dir.join(PROXYWARDEN_COMPONENTS_DIR_NAME)
}
pub fn proxifyre_install_dir_from_app_dir(app_dir: &Path) -> PathBuf {
component_root_from_app_dir(app_dir).join(PROXIFYRE_COMPONENT_DIR_NAME)
}
pub fn singbox_install_dir_from_app_dir(app_dir: &Path) -> PathBuf {
component_root_from_app_dir(app_dir).join(SINGBOX_COMPONENT_DIR_NAME)
}
pub fn default_proxifyre_install_dir() -> PathBuf {
app_install_dir_from_current_exe()
.map(|app_dir| proxifyre_install_dir_from_app_dir(&app_dir))
.unwrap_or_else(|| {
PathBuf::from(r"C:\Program Files\ProxyWarden")
.join(PROXYWARDEN_COMPONENTS_DIR_NAME)
.join(PROXIFYRE_COMPONENT_DIR_NAME)
})
}
pub fn default_singbox_install_dir() -> PathBuf {
app_install_dir_from_current_exe()
.map(|app_dir| singbox_install_dir_from_app_dir(&app_dir))
.unwrap_or_else(|| PathBuf::from(DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT))
}
pub fn detect_proxyfier_install_with_host(
host: &impl ProxyfierDetectionHost,
) -> Option<DetectedProxyfier> {
let proxifyre_running = host.process_running("ProxiFyre.exe")
|| host.service_running("ProxiFyreService")
|| host.service_running("ProxiFyre");
let detected_service = detect_proxifyre_service(host);
let proxifyre_running = detected_service
.as_ref()
.is_some_and(|service| service_status_is_running(&service.status));
proxyfier_candidates(host)
.into_iter()
.filter_map(|candidate| candidate.into_detected(host, proxifyre_running))
.filter_map(|candidate| {
candidate.into_detected(host, proxifyre_running, detected_service.as_ref())
})
.next()
}
@@ -161,6 +214,15 @@ fn detected_proxyfier_component(proxyfier: &DetectedProxyfier) -> ComponentStatu
}
}
};
let service_name = proxyfier
.service_name
.clone()
.or_else(|| service_name(&proxyfier.engine).map(str::to_string));
let service_status = proxyfier.service_status.clone();
let mut problems = Vec::new();
if service_status.is_none() {
problems.push("Служба ProxiFyre не установлена".to_string());
}
ComponentStatus {
id: ComponentId::Proxyfier,
@@ -169,10 +231,16 @@ fn detected_proxyfier_component(proxyfier: &DetectedProxyfier) -> ComponentStatu
installed: true,
running: proxyfier.running,
version: Some(match proxyfier.engine {
ProxyfierEngine::ProxiFyre => "ProxiFyre найден".to_string(),
ProxyfierEngine::ProxiFyre => match service_status.as_deref() {
Some(status) if service_status_is_running(status) => "служба запущена".to_string(),
Some(_) => "служба остановлена".to_string(),
None => "служба не установлена".to_string(),
},
}),
path: Some(proxyfier.install_dir.display().to_string()),
problems: Vec::new(),
service_name,
service_status,
problems,
actions,
}
}
@@ -186,6 +254,8 @@ fn missing_proxyfier_component() -> ComponentStatus {
running: false,
version: None,
path: None,
service_name: service_name(&ProxyfierEngine::ProxiFyre).map(str::to_string),
service_status: None,
problems: vec!["ProxiFyre нужен для маршрутизации выбранных приложений".to_string()],
actions: vec!["Установить ProxiFyre".to_string()],
}
@@ -224,6 +294,15 @@ fn detected_singbox_component(singbox: &DetectedSingBox) -> ComponentStatus {
running: singbox.running,
version: Some("sing-box найден".to_string()),
path: Some(singbox.executable_path.display().to_string()),
service_name: Some(singbox.service_name.clone()),
service_status: Some(
if singbox.running {
"running"
} else {
"stopped"
}
.to_string(),
),
problems,
actions,
}
@@ -238,6 +317,8 @@ fn missing_singbox_component() -> ComponentStatus {
running: false,
version: None,
path: None,
service_name: Some(DEFAULT_LOCAL_SINGBOX_SERVICE_NAME.to_string()),
service_status: None,
problems: Vec::new(),
actions: vec!["Установить Local sing-box".to_string()],
}
@@ -255,21 +336,19 @@ impl ProxyfierCandidate {
self,
host: &impl ProxyfierDetectionHost,
proxifyre_running: bool,
detected_service: Option<&DetectedService>,
) -> Option<DetectedProxyfier> {
let executable_path = self.install_dir.join(executable_name(&self.engine));
let config_path = config_path(&self.engine, &self.install_dir);
let exists = host.path_exists(&self.install_dir)
|| host.path_exists(&executable_path)
|| config_path
.as_ref()
.is_some_and(|path| host.path_exists(path));
if !exists {
if !host.path_exists(&executable_path) {
return None;
}
Some(DetectedProxyfier {
service_name: service_name(&self.engine).map(str::to_string),
service_name: detected_service
.map(|service| service.name.clone())
.or_else(|| service_name(&self.engine).map(str::to_string)),
service_status: detected_service.map(|service| service.status.clone()),
engine: self.engine,
name: self.name,
install_dir: self.install_dir,
@@ -290,6 +369,14 @@ fn proxyfier_candidates(host: &impl ProxyfierDetectionHost) -> Vec<ProxyfierCand
"ProxiFyre",
"PROXYWARDEN_PROXIFYRE_ROOT",
);
push_candidate(
&mut candidates,
ProxyfierCandidate {
engine: ProxyfierEngine::ProxiFyre,
name: "ProxiFyre".to_string(),
install_dir: default_proxifyre_install_dir(),
},
);
for entry in host.registry_install_entries() {
if let Some(engine) = engine_from_name(&entry.display_name) {
let install_dir = entry
@@ -350,11 +437,17 @@ fn push_candidate(candidates: &mut Vec<ProxyfierCandidate>, candidate: Proxyfier
}
fn common_install_dirs(host: &impl ProxyfierDetectionHost, folder_name: &str) -> Vec<PathBuf> {
let mut dirs = vec![PathBuf::from(format!(r"C:\Tools\{folder_name}"))];
let mut dirs = Vec::new();
for env_name in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] {
if let Some(root) = host.env_var(env_name) {
dirs.push(PathBuf::from(root).join(folder_name));
let proxywarden_root = PathBuf::from(root).join("ProxyWarden");
dirs.push(
proxywarden_root
.join(PROXYWARDEN_COMPONENTS_DIR_NAME)
.join(folder_name),
);
dirs.push(proxywarden_root.join(folder_name));
}
}
@@ -367,21 +460,26 @@ fn singbox_candidates(host: &impl ProxyfierDetectionHost) -> Vec<PathBuf> {
if let Some(path) = host.env_var("PROXYWARDEN_SINGBOX_ROOT") {
push_path_candidate(&mut candidates, PathBuf::from(path));
}
push_path_candidate(&mut candidates, default_singbox_install_dir());
push_path_candidate(
&mut candidates,
PathBuf::from(DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT),
);
push_path_candidate(
&mut candidates,
PathBuf::from(r"C:\Tools\ProxyWarden\sing-box"),
);
for env_name in ["ProgramFiles", "ProgramFiles(x86)", "LOCALAPPDATA"] {
if let Some(root) = host.env_var(env_name) {
push_path_candidate(
&mut candidates,
PathBuf::from(&root).join("ProxyWarden").join("sing-box"),
PathBuf::from(&root)
.join("ProxyWarden")
.join(PROXYWARDEN_COMPONENTS_DIR_NAME)
.join(SINGBOX_COMPONENT_DIR_NAME),
);
push_path_candidate(
&mut candidates,
PathBuf::from(&root)
.join("ProxyWarden")
.join(SINGBOX_COMPONENT_DIR_NAME),
);
push_path_candidate(&mut candidates, PathBuf::from(root).join("sing-box"));
}
}
@@ -441,6 +539,27 @@ fn service_name(engine: &ProxyfierEngine) -> Option<&'static str> {
}
}
fn detect_proxifyre_service(host: &impl ProxyfierDetectionHost) -> Option<DetectedService> {
for name in ["ProxiFyreService", "ProxiFyre"] {
if let Some(status) = host.service_status(name) {
return Some(DetectedService {
name: name.to_string(),
status: normalize_service_status(&status),
});
}
}
None
}
fn normalize_service_status(status: &str) -> String {
status.trim().to_ascii_lowercase()
}
fn service_status_is_running(status: &str) -> bool {
status.trim().eq_ignore_ascii_case("running")
}
fn engine_from_name(name: &str) -> Option<ProxyfierEngine> {
let normalized = name.to_ascii_lowercase();
if normalized.contains("proxifyre") {
@@ -459,6 +578,17 @@ fn same_path(left: &Path, right: &Path) -> bool {
.eq_ignore_ascii_case(&right.to_string_lossy())
}
fn powershell_text(script: &str) -> Option<String> {
command_no_window("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", script])
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| String::from_utf8(output.stdout).ok())
.map(|stdout| stdout.trim().to_string())
.filter(|stdout| !stdout.is_empty())
}
fn powershell_bool(script: &str) -> bool {
command_no_window("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", script])
+1
View File
@@ -33,6 +33,7 @@ pub fn run() {
commands::save_target,
commands::get_components,
commands::get_proxifyre_setup_status,
commands::get_proxifyre_setup_progress,
commands::get_singbox_status,
commands::get_singbox_setup_status,
commands::resolve_profile_preview,
+6 -1
View File
@@ -6,7 +6,8 @@ use url::Url;
pub const DEFAULT_LOCAL_SINGBOX_LISTEN_HOST: &str = "127.0.0.1";
pub const DEFAULT_LOCAL_SINGBOX_LISTEN_PORT: u16 = 1080;
pub const DEFAULT_LOCAL_SINGBOX_SERVICE_NAME: &str = "ProxyWardenSingBox";
pub const DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT: &str = r"C:\Program Files\ProxyWarden\sing-box";
pub const DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT: &str =
r"C:\Program Files\ProxyWarden\components\sing-box";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
@@ -131,6 +132,10 @@ pub struct ComponentStatus {
pub version: Option<String>,
pub path: Option<String>,
#[serde(default)]
pub service_name: Option<String>,
#[serde(default)]
pub service_status: Option<String>,
#[serde(default)]
pub problems: Vec<String>,
#[serde(default)]
pub actions: Vec<String>,
+16 -5
View File
@@ -1,7 +1,7 @@
use crate::component_detection::DetectedSingBox;
use crate::models::{DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT, DEFAULT_LOCAL_SINGBOX_SERVICE_NAME};
use serde::{Deserialize, Serialize};
use std::path::Path;
use std::path::{Path, PathBuf};
pub const WINSW_WRAPPER_FILE: &str = "ProxyWardenSingBox.exe";
@@ -56,9 +56,19 @@ pub struct ServiceCommandOutput {
}
pub fn build_singbox_setup_status(detected: Option<&DetectedSingBox>) -> SingBoxSetupStatus {
build_singbox_setup_status_with_install_root(
detected,
&PathBuf::from(DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT),
)
}
pub fn build_singbox_setup_status_with_install_root(
detected: Option<&DetectedSingBox>,
default_install_root: &Path,
) -> SingBoxSetupStatus {
let install_root = detected
.map(|singbox| singbox.install_dir.display().to_string())
.unwrap_or_else(|| DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT.to_string());
.unwrap_or_else(|| default_install_root.display().to_string());
let binary_item = match detected {
Some(singbox) if singbox.binary_exists => SingBoxSetupItem {
id: "sing-box-binary".to_string(),
@@ -152,9 +162,10 @@ pub fn ensure_safe_singbox_install_dir(path: &Path) -> Result<(), String> {
.unwrap_or_default()
.to_ascii_lowercase();
if file_name == "sing-box"
&& (normalized.contains("\\proxywarden\\") || normalized.contains("\\proxywarden\\"))
{
let is_proxywarden_component = normalized.contains("\\proxywarden\\components\\");
let is_legacy_proxywarden_child = normalized.ends_with("\\proxywarden\\sing-box");
if file_name == "sing-box" && (is_proxywarden_component || is_legacy_proxywarden_child) {
return Ok(());
}