Refine ProxyWarden routing and config flows
This commit is contained in:
Generated
+1
-1
@@ -2314,7 +2314,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proxywarden"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"percent-encoding",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proxywarden"
|
||||
version = "1.0.2"
|
||||
version = "1.0.3"
|
||||
description = "Standalone Windows desktop proxy management app for ProxyWarden."
|
||||
authors = ["ProxyWarden"]
|
||||
edition = "2021"
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
param(
|
||||
[string]$InstallRoot = "",
|
||||
[switch]$ForceRemoveWindowsPacketFilter
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function New-Result {
|
||||
param(
|
||||
[bool]$Success,
|
||||
[string]$Message,
|
||||
[hashtable]$Details = @{}
|
||||
)
|
||||
|
||||
[ordered]@{
|
||||
success = $Success
|
||||
message = $Message
|
||||
details = $Details
|
||||
} | ConvertTo-Json -Depth 8 -Compress
|
||||
}
|
||||
|
||||
function Get-FullPath([string]$Path) {
|
||||
return [System.IO.Path]::GetFullPath($Path).TrimEnd("\")
|
||||
}
|
||||
|
||||
function Test-PathInside([string]$Path, [string]$Root) {
|
||||
if ([string]::IsNullOrWhiteSpace($Path)) { return $false }
|
||||
try {
|
||||
$fullPath = Get-FullPath $Path
|
||||
$fullRoot = Get-FullPath $Root
|
||||
return $fullPath.StartsWith($fullRoot + "\", [StringComparison]::OrdinalIgnoreCase)
|
||||
} catch {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-SafeInstallRoot([string]$Root) {
|
||||
if ([string]::IsNullOrWhiteSpace($Root)) {
|
||||
throw "InstallRoot is empty."
|
||||
}
|
||||
|
||||
$full = Get-FullPath $Root
|
||||
if ($full -match "^[A-Za-z]:\\?$") {
|
||||
throw "Refusing to use drive root as InstallRoot: $full"
|
||||
}
|
||||
if ($full -match "\\Windows($|\\)" -or $full -match "\\ProgramData$" -or $full -match "\\Users$") {
|
||||
throw "Refusing unsafe InstallRoot: $full"
|
||||
}
|
||||
|
||||
$knownAppFiles = @(
|
||||
(Join-Path $full "proxywarden.exe"),
|
||||
(Join-Path $full "uninstall.exe"),
|
||||
(Join-Path $full "bundled\cleanup\uninstall-managed-components.ps1")
|
||||
)
|
||||
foreach ($candidate in $knownAppFiles) {
|
||||
if (Test-Path -LiteralPath $candidate) { return $full }
|
||||
}
|
||||
|
||||
throw "InstallRoot does not look like a ProxyWarden install directory: $full"
|
||||
}
|
||||
|
||||
function Resolve-SafeComponentDir([string]$Root, [string]$Leaf) {
|
||||
$componentRoot = Join-Path $Root "components"
|
||||
$path = Join-Path $componentRoot $Leaf
|
||||
$full = Get-FullPath $path
|
||||
$expectedParent = Get-FullPath $componentRoot
|
||||
$actualLeaf = Split-Path -Leaf $full
|
||||
|
||||
if ($actualLeaf -ne $Leaf) {
|
||||
throw "Unexpected component directory leaf: $full"
|
||||
}
|
||||
if (-not $full.StartsWith($expectedParent + "\", [StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Component directory is outside ProxyWarden components root: $full"
|
||||
}
|
||||
|
||||
return $full
|
||||
}
|
||||
|
||||
function Read-ComponentMarker([string]$Dir) {
|
||||
$markerPath = Join-Path $Dir "proxywarden-component.json"
|
||||
if (-not (Test-Path -LiteralPath $markerPath)) { return $null }
|
||||
try {
|
||||
return Get-Content -LiteralPath $markerPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
} catch {
|
||||
return $null
|
||||
}
|
||||
}
|
||||
|
||||
function Get-MarkerBool($Marker, [string]$Name) {
|
||||
if ($null -eq $Marker) { return $false }
|
||||
$property = $Marker.PSObject.Properties[$Name]
|
||||
if ($null -eq $property) { return $false }
|
||||
return [bool]$property.Value
|
||||
}
|
||||
|
||||
function Get-ServiceRecord([string]$Name) {
|
||||
$escaped = $Name.Replace("'", "''")
|
||||
return Get-CimInstance Win32_Service -Filter "Name='$escaped'" -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Get-ServiceImagePath($Record) {
|
||||
if ($null -eq $Record -or [string]::IsNullOrWhiteSpace([string]$Record.PathName)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$pathName = ([string]$Record.PathName).Trim()
|
||||
if ($pathName -match '^"([^"]+)"') { return $Matches[1] }
|
||||
if ($pathName -match '^(.+?\.exe)\b') { return $Matches[1].Trim() }
|
||||
return $pathName
|
||||
}
|
||||
|
||||
function Stop-ServiceRecord($Record) {
|
||||
if ($null -eq $Record) { return }
|
||||
|
||||
$service = Get-Service -Name $Record.Name -ErrorAction SilentlyContinue
|
||||
if ($null -ne $service -and $service.Status -ne "Stopped") {
|
||||
Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue
|
||||
$service = Get-Service -Name $Record.Name -ErrorAction SilentlyContinue
|
||||
if ($null -ne $service) {
|
||||
try { $service.WaitForStatus("Stopped", [TimeSpan]::FromSeconds(12)) } catch {}
|
||||
}
|
||||
}
|
||||
|
||||
$recordAfterStop = Get-ServiceRecord $Record.Name
|
||||
if ($null -ne $recordAfterStop -and [int]$recordAfterStop.ProcessId -gt 0) {
|
||||
taskkill.exe /PID ([int]$recordAfterStop.ProcessId) /F | Out-Null
|
||||
Start-Sleep -Milliseconds 500
|
||||
}
|
||||
}
|
||||
|
||||
function Remove-ManagedService {
|
||||
param(
|
||||
[string[]]$Names,
|
||||
[string]$InstallRoot,
|
||||
[string]$UninstallExe = ""
|
||||
)
|
||||
|
||||
$removed = @()
|
||||
foreach ($name in $Names) {
|
||||
$record = Get-ServiceRecord $name
|
||||
if ($null -eq $record) { continue }
|
||||
|
||||
$imagePath = Get-ServiceImagePath $record
|
||||
if (-not [string]::IsNullOrWhiteSpace($imagePath) -and -not (Test-PathInside $imagePath $InstallRoot)) {
|
||||
continue
|
||||
}
|
||||
|
||||
Stop-ServiceRecord $record
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($UninstallExe) -and (Test-Path -LiteralPath $UninstallExe)) {
|
||||
Push-Location (Split-Path -Parent $UninstallExe)
|
||||
try { & $UninstallExe uninstall | Out-Null } finally { Pop-Location }
|
||||
}
|
||||
|
||||
$record = Get-ServiceRecord $name
|
||||
if ($null -ne $record) {
|
||||
sc.exe delete $name | Out-Null
|
||||
}
|
||||
$removed += $name
|
||||
}
|
||||
|
||||
return $removed
|
||||
}
|
||||
|
||||
function Remove-SafeDirectory([string]$Path, [string]$Root) {
|
||||
if (-not (Test-Path -LiteralPath $Path)) { return $false }
|
||||
if (-not (Test-PathInside $Path $Root)) {
|
||||
throw "Refusing to remove directory outside InstallRoot: $Path"
|
||||
}
|
||||
Remove-Item -LiteralPath $Path -Recurse -Force
|
||||
return $true
|
||||
}
|
||||
|
||||
function Get-InstalledProgram([string]$Pattern) {
|
||||
$paths = @(
|
||||
"HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*",
|
||||
"HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
|
||||
)
|
||||
return Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.DisplayName -match $Pattern } |
|
||||
Select-Object -First 1 DisplayName, DisplayVersion, PSChildName, UninstallString, QuietUninstallString
|
||||
}
|
||||
|
||||
function Resolve-MsiProductCode($Program, [string]$Label) {
|
||||
if ($null -eq $Program) { return $null }
|
||||
if ($Program.PSChildName -match "^\{[0-9A-Fa-f-]{36}\}$") {
|
||||
return $Program.PSChildName
|
||||
}
|
||||
foreach ($candidate in @($Program.QuietUninstallString, $Program.UninstallString)) {
|
||||
if ($candidate -match "\{[0-9A-Fa-f-]{36}\}") {
|
||||
return $Matches[0]
|
||||
}
|
||||
}
|
||||
throw "Could not resolve MSI product code for $Label."
|
||||
}
|
||||
|
||||
function Uninstall-MsiProgram($Program, [string]$Label) {
|
||||
$productCode = Resolve-MsiProductCode $Program $Label
|
||||
if ([string]::IsNullOrWhiteSpace($productCode)) { return $false }
|
||||
|
||||
$logPath = Join-Path ([System.IO.Path]::GetTempPath()) "proxywarden-$Label-uninstall.log"
|
||||
$process = Start-Process -FilePath "msiexec.exe" -ArgumentList @("/x", $productCode, "/qn", "/norestart", "/L*v", $logPath) -Wait -PassThru -WindowStyle Hidden
|
||||
if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010 -and $process.ExitCode -ne 1605) {
|
||||
throw "$Label uninstall exited with code $($process.ExitCode). MSI log: $logPath"
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
|
||||
try {
|
||||
$details = @{}
|
||||
$root = Assert-SafeInstallRoot $InstallRoot
|
||||
$details.installRoot = $root
|
||||
|
||||
$proxifyreDir = Resolve-SafeComponentDir $root "ProxiFyre"
|
||||
$singboxDir = Resolve-SafeComponentDir $root "sing-box"
|
||||
$proxifyreMarker = Read-ComponentMarker $proxifyreDir
|
||||
$removePacketFilter = [bool]$ForceRemoveWindowsPacketFilter -or (Get-MarkerBool $proxifyreMarker "packetFilterInstalledByProxyWarden")
|
||||
|
||||
$details.removedProxiFyreServices = Remove-ManagedService -Names @("ProxiFyreService", "ProxiFyre") -InstallRoot $root -UninstallExe (Join-Path $proxifyreDir "ProxiFyre.exe")
|
||||
$details.removedSingBoxServices = Remove-ManagedService -Names @("ProxyWardenSingBox") -InstallRoot $root -UninstallExe (Join-Path $singboxDir "ProxyWardenSingBox.exe")
|
||||
$details.removedProxiFyreDir = Remove-SafeDirectory $proxifyreDir $root
|
||||
$details.removedSingBoxDir = Remove-SafeDirectory $singboxDir $root
|
||||
|
||||
if ($removePacketFilter) {
|
||||
$packetFilter = Get-InstalledProgram "Windows Packet Filter|WinpkFilter|NDISAPI"
|
||||
$details.removedWindowsPacketFilter = Uninstall-MsiProgram $packetFilter "windows-packet-filter"
|
||||
} else {
|
||||
$details.removedWindowsPacketFilter = $false
|
||||
}
|
||||
|
||||
New-Result -Success $true -Message "ProxyWarden managed components cleanup completed." -Details $details
|
||||
exit 0
|
||||
} catch {
|
||||
New-Result -Success $false -Message $_.Exception.Message -Details @{}
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
!macro NSIS_HOOK_PREUNINSTALL
|
||||
DetailPrint "ProxyWarden: cleaning managed components"
|
||||
nsExec::ExecToLog 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\bundled\cleanup\uninstall-managed-components.ps1" -InstallRoot "$INSTDIR"'
|
||||
Pop $0
|
||||
DetailPrint "ProxyWarden cleanup exit code: $0"
|
||||
!macroend
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"generatedAt": "2026-07-09T16:13:27.3087159Z",
|
||||
"architectures": [
|
||||
"x64"
|
||||
],
|
||||
"proxifyreRelease": "v2.2.1",
|
||||
"windowsPacketFilterRelease": "v3.6.2",
|
||||
"files": [
|
||||
{
|
||||
"id": "proxifyre-x64",
|
||||
"name": "ProxiFyre-v2.2.1-x64-signed.zip",
|
||||
"sha256": "c38ca1caa68cd730712f5c0911e4240711bf9e7684988ae64ed04ec693cce899",
|
||||
"size": 1372483,
|
||||
"sourceUrl": "https://github.com/wiresock/proxifyre/releases/download/v2.2.1/ProxiFyre-v2.2.1-x64-signed.zip"
|
||||
},
|
||||
{
|
||||
"id": "packet-filter-x64",
|
||||
"name": "Windows.Packet.Filter.3.6.2.1.x64.msi",
|
||||
"sha256": "9c388c0b7f189f7fa98720bae2caecf7d64f30910838b80b438ecf8956b8502c",
|
||||
"size": 819200,
|
||||
"sourceUrl": "https://github.com/wiresock/ndisapi/releases/download/v3.6.2/Windows.Packet.Filter.3.6.2.1.x64.msi"
|
||||
},
|
||||
{
|
||||
"id": "vc-runtime-x64",
|
||||
"name": "vc_redist.x64.exe",
|
||||
"sha256": "843068991daaa1f73ad9f6239bce4d0f6a07a51f18c37ea2a867e9beca71295c",
|
||||
"size": 18731856,
|
||||
"sourceUrl": "https://aka.ms/vc14/vc_redist.x64.exe"
|
||||
}
|
||||
]
|
||||
}
|
||||
Binary file not shown.
+671
-66
File diff suppressed because it is too large
Load Diff
@@ -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])
|
||||
|
||||
@@ -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,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>,
|
||||
|
||||
@@ -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(());
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "ProxyWarden",
|
||||
"version": "1.0.2",
|
||||
"version": "1.0.3",
|
||||
"identifier": "ru.dokops.proxywarden.windows",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
@@ -27,7 +27,17 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"targets": "nsis",
|
||||
"resources": [
|
||||
"bundled/proxifyre",
|
||||
"bundled/cleanup"
|
||||
],
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installMode": "perMachine",
|
||||
"installerHooks": "bundled/installer-hooks/proxywarden-hooks.nsh"
|
||||
}
|
||||
},
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
|
||||
@@ -258,6 +258,9 @@ fn proxifyre_install_script_uses_resilient_download_helpers() {
|
||||
|
||||
assert!(script.contains("function Get-SafeUriForLog([string]$uri)"));
|
||||
assert!(script.contains("function Invoke-ReleaseApi([string]$uri, [string]$label)"));
|
||||
assert!(script.contains("function Resolve-ReleaseAsset("));
|
||||
assert!(script.contains("function Get-PinnedWindowsPacketFilterAsset([string]$arch)"));
|
||||
assert!(script.contains("function Get-PinnedProxiFyreAsset([string]$arch)"));
|
||||
assert!(
|
||||
script.contains("function Invoke-Download([string]$uri, [string]$path, [string]$label)")
|
||||
);
|
||||
@@ -266,20 +269,150 @@ fn proxifyre_install_script_uses_resilient_download_helpers() {
|
||||
assert!(script.contains("Invoke-CurlDownload $uri $partialPath"));
|
||||
assert!(script.contains("--user-agent 'proxywarden' --output $partialPath --url $uri"));
|
||||
assert!(script.contains("Move-Item -LiteralPath $partialPath -Destination $path -Force"));
|
||||
assert!(script.contains("function Get-BundledAsset([string]$pattern, [string]$label)"));
|
||||
assert!(script.contains("function Verify-BundledAssetHash([string]$path, [string]$label)"));
|
||||
assert!(script
|
||||
.contains("Invoke-Download $vcRedistUrl $vcRedistPath 'Microsoft Visual C++ Runtime'"));
|
||||
assert!(script.contains("Invoke-ReleaseApi $ndisapiReleaseApi 'Windows Packet Filter'"));
|
||||
assert!(script
|
||||
.contains("Resolve-ReleaseAsset $ndisapiReleaseApi $ndisPattern 'Windows Packet Filter'"));
|
||||
assert!(script.contains(
|
||||
"Invoke-Download $ndisAsset.browser_download_url $ndisPath 'Windows Packet Filter'"
|
||||
));
|
||||
assert!(script.contains("Invoke-ReleaseApi $proxifyreReleaseApi 'ProxiFyre'"));
|
||||
assert!(
|
||||
script.contains("Resolve-ReleaseAsset $proxifyreReleaseApi $proxifyrePattern 'ProxiFyre'")
|
||||
);
|
||||
assert!(script.contains(
|
||||
"Invoke-Download $proxifyreAsset.browser_download_url $proxifyreZipPath 'ProxiFyre'"
|
||||
));
|
||||
assert!(script.contains("github.com/wiresock/ndisapi/releases/download"));
|
||||
assert!(script.contains("github.com/wiresock/proxifyre/releases/download"));
|
||||
let packet_filter_step = script
|
||||
.find("Write-ProxyWardenProgress 'install' 'packet-filter'")
|
||||
.expect("packet filter install step should be present");
|
||||
let vc_runtime_step = script
|
||||
.find("Write-ProxyWardenProgress 'install' 'vc-runtime'")
|
||||
.expect("runtime install step should be present");
|
||||
let proxifyre_step = script
|
||||
.find("Write-ProxyWardenProgress 'install' 'proxifyre'")
|
||||
.expect("proxifyre install step should be present");
|
||||
assert!(packet_filter_step < vc_runtime_step);
|
||||
assert!(vc_runtime_step < proxifyre_step);
|
||||
|
||||
cleanup(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxifyre_install_script_prefers_bundled_assets_before_downloads() {
|
||||
let root = test_root("proxifyre-install-script-bundled-assets");
|
||||
let bundle_dir = root.join("bundle");
|
||||
let script = commands::install_proxifyre_script_with_bundle(
|
||||
&root.join("proxifyre-app-config.json"),
|
||||
Some(&bundle_dir),
|
||||
);
|
||||
|
||||
assert!(script.contains(&format!(
|
||||
"$bundledAssetDir = '{}'",
|
||||
bundle_dir.display().to_string().replace('\'', "''")
|
||||
)));
|
||||
assert!(script.contains("$script:bundledAssetDir = [string]$bundledAssetDir"));
|
||||
assert!(script.contains("function Get-BundledAssetDir"));
|
||||
assert!(script.contains("$manifestPath = [IO.Path]::Combine($assetDir, 'manifest.json')"));
|
||||
assert!(script.contains("$script:bundledAssetManifest = Get-BundledAssetManifest"));
|
||||
assert!(script.contains("Copy-BundledAsset $bundledNdisPath $ndisPath"));
|
||||
assert!(script.contains("Copy-BundledAsset $bundledVcPath $vcRedistPath"));
|
||||
assert!(script.contains("Copy-BundledAsset $bundledProxiFyrePath $proxifyreZipPath"));
|
||||
|
||||
let bundled_ndis = script
|
||||
.find("Get-BundledAsset $ndisPattern 'Windows Packet Filter'")
|
||||
.expect("ndis bundle check should be present");
|
||||
let online_ndis = script
|
||||
.find("Resolve-ReleaseAsset $ndisapiReleaseApi $ndisPattern 'Windows Packet Filter'")
|
||||
.expect("ndis online fallback should be present");
|
||||
assert!(bundled_ndis < online_ndis);
|
||||
|
||||
let bundled_proxifyre = script
|
||||
.find("Get-BundledAsset $proxifyrePattern 'ProxiFyre'")
|
||||
.expect("proxifyre bundle check should be present");
|
||||
let online_proxifyre = script
|
||||
.find("Resolve-ReleaseAsset $proxifyreReleaseApi $proxifyrePattern 'ProxiFyre'")
|
||||
.expect("proxifyre online fallback should be present");
|
||||
assert!(bundled_proxifyre < online_proxifyre);
|
||||
|
||||
cleanup(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(windows)]
|
||||
fn proxifyre_uninstall_script_parses_as_powershell() {
|
||||
let root = test_root("proxifyre-uninstall-script");
|
||||
fs::create_dir_all(&root).expect("test root should be created");
|
||||
let detected = DetectedProxyfier {
|
||||
engine: ProxyfierEngine::ProxiFyre,
|
||||
name: "ProxiFyre".to_string(),
|
||||
install_dir: root.join("ProxiFyre"),
|
||||
executable_path: root.join("ProxiFyre").join("ProxiFyre.exe"),
|
||||
config_path: Some(root.join("ProxiFyre").join("app-config.json")),
|
||||
running: false,
|
||||
service_name: Some("ProxiFyreService".to_string()),
|
||||
service_status: Some("stopped".to_string()),
|
||||
};
|
||||
|
||||
let script = commands::wrap_elevated_package_script(
|
||||
&commands::uninstall_proxifyre_script(Some(&detected)),
|
||||
&root.join("uninstall.log"),
|
||||
);
|
||||
let script_path = root.join("uninstall.ps1");
|
||||
let mut script_bytes = vec![0xEF, 0xBB, 0xBF];
|
||||
script_bytes.extend_from_slice(script.as_bytes());
|
||||
fs::write(&script_path, script_bytes).expect("script should be written");
|
||||
|
||||
let escaped_path = script_path.display().to_string().replace('\'', "''");
|
||||
let parser = format!(
|
||||
"$tokens = $null; $errors = $null; [System.Management.Automation.Language.Parser]::ParseFile('{escaped_path}', [ref]$tokens, [ref]$errors) | Out-Null; if ($errors.Count -gt 0) {{ $errors | ForEach-Object {{ $_.Message }}; exit 1 }}"
|
||||
);
|
||||
let output = ProcessCommand::new("powershell")
|
||||
.args(["-NoProfile", "-NonInteractive", "-Command", &parser])
|
||||
.output()
|
||||
.expect("powershell parser should run");
|
||||
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"uninstall script should parse\nstdout:\n{}\nstderr:\n{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
|
||||
cleanup(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn proxifyre_uninstall_script_removes_packet_filter_after_proxifyre() {
|
||||
let detected = DetectedProxyfier {
|
||||
engine: ProxyfierEngine::ProxiFyre,
|
||||
name: "ProxiFyre".to_string(),
|
||||
install_dir: PathBuf::from(r"C:\Tools\ProxiFyre"),
|
||||
executable_path: PathBuf::from(r"C:\Tools\ProxiFyre\ProxiFyre.exe"),
|
||||
config_path: Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json")),
|
||||
running: true,
|
||||
service_name: Some("ProxiFyreService".to_string()),
|
||||
service_status: Some("running".to_string()),
|
||||
};
|
||||
let script = commands::uninstall_proxifyre_script(Some(&detected));
|
||||
|
||||
assert!(script.contains("function Resolve-MsiProductCode($program, [string]$label)"));
|
||||
assert!(script.contains("Отказываюсь запускать произвольный UninstallString"));
|
||||
assert!(script.contains("Start-Process -FilePath 'msiexec.exe'"));
|
||||
assert!(script.contains("ArgumentList @('/x', $productCode, '/qn', '/norestart'"));
|
||||
assert!(script.contains("Uninstall-MsiProgram $packetFilter 'Windows Packet Filter'"));
|
||||
let proxifyre_step = script
|
||||
.find("Write-ProxyWardenProgress 'uninstall' 'proxifyre'")
|
||||
.expect("proxifyre uninstall step should be present");
|
||||
let packet_filter_step = script
|
||||
.find("Write-ProxyWardenProgress 'uninstall' 'packet-filter'")
|
||||
.expect("packet filter uninstall step should be present");
|
||||
assert!(proxifyre_step < packet_filter_step);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn singbox_runner_preserves_installer_args_with_spaces() {
|
||||
let script = commands::singbox_installer_runner_script(
|
||||
@@ -287,15 +420,16 @@ fn singbox_runner_preserves_installer_args_with_spaces() {
|
||||
Path::new(r"C:\ProgramData\ProxyWarden\state\install.log"),
|
||||
&[
|
||||
"-InstallRoot".to_string(),
|
||||
r"C:\Program Files\ProxyWarden\sing-box".to_string(),
|
||||
r"C:\Program Files\ProxyWarden\components\sing-box".to_string(),
|
||||
"-ServiceName".to_string(),
|
||||
"ProxyWardenSingBox".to_string(),
|
||||
"-Uninstall".to_string(),
|
||||
],
|
||||
);
|
||||
|
||||
assert!(script
|
||||
.contains("$installerArgs = @('-InstallRoot', 'C:\\Program Files\\ProxyWarden\\sing-box'"));
|
||||
assert!(script.contains(
|
||||
"$installerArgs = @('-InstallRoot', 'C:\\Program Files\\ProxyWarden\\components\\sing-box'"
|
||||
));
|
||||
assert!(script.contains(
|
||||
"& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $installerPath @installerArgs"
|
||||
));
|
||||
@@ -390,6 +524,7 @@ fn component_status_merges_detected_existing_proxifyre() {
|
||||
config_path: Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json")),
|
||||
running: true,
|
||||
service_name: Some("ProxiFyreService".to_string()),
|
||||
service_status: Some("running".to_string()),
|
||||
}),
|
||||
None,
|
||||
);
|
||||
@@ -540,8 +675,8 @@ impl ProxyfierDetectionHost for DetectionHost {
|
||||
false
|
||||
}
|
||||
|
||||
fn service_running(&self, _service_name: &str) -> bool {
|
||||
false
|
||||
fn service_status(&self, _service_name: &str) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {
|
||||
@@ -625,6 +760,8 @@ fn proxyfier_running() -> ComponentStatus {
|
||||
running: true,
|
||||
version: Some("2.2.1".to_string()),
|
||||
path: Some(r"C:\Tools\ProxiFyre".to_string()),
|
||||
service_name: Some("ProxiFyreService".to_string()),
|
||||
service_status: Some("running".to_string()),
|
||||
problems: Vec::new(),
|
||||
actions: vec!["Restart".to_string()],
|
||||
}
|
||||
@@ -639,6 +776,8 @@ fn singbox_missing() -> ComponentStatus {
|
||||
running: false,
|
||||
version: None,
|
||||
path: None,
|
||||
service_name: Some("ProxyWardenSingBox".to_string()),
|
||||
service_status: None,
|
||||
problems: vec!["Локальный sing-box не установлен".to_string()],
|
||||
actions: vec!["Установить локальный sing-box".to_string()],
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ fn detects_existing_proxifyre_from_registry_install_location() {
|
||||
let host = MockHost::new()
|
||||
.with_registry("ProxiFyre", r"C:\Tools\ProxiFyre")
|
||||
.with_path(r"C:\Tools\ProxiFyre")
|
||||
.with_path(r"C:\Tools\ProxiFyre\ProxiFyre.exe")
|
||||
.with_service("ProxiFyreService");
|
||||
|
||||
let detected = detect_proxyfier_install_with_host(&host)
|
||||
@@ -26,15 +27,30 @@ fn detects_existing_proxifyre_from_registry_install_location() {
|
||||
Some(PathBuf::from(r"C:\Tools\ProxiFyre\app-config.json"))
|
||||
);
|
||||
assert!(detected.running);
|
||||
assert_eq!(detected.service_name, Some("ProxiFyreService".to_string()));
|
||||
assert_eq!(detected.service_status, Some("running".to_string()));
|
||||
|
||||
let component = proxyfier_component_from_detection(Some(&detected));
|
||||
assert_eq!(component.state, ComponentState::Running);
|
||||
assert!(component.installed);
|
||||
assert!(component.running);
|
||||
assert_eq!(component.path, Some(r"C:\Tools\ProxiFyre".to_string()));
|
||||
assert_eq!(component.service_name, Some("ProxiFyreService".to_string()));
|
||||
assert_eq!(component.service_status, Some("running".to_string()));
|
||||
assert!(component.problems.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_empty_common_proxifyre_folder_without_executable() {
|
||||
let host = MockHost::new().with_path(r"C:\Tools\ProxiFyre");
|
||||
|
||||
assert!(detect_proxyfier_install_with_host(&host).is_none());
|
||||
|
||||
let component = proxyfier_component_from_detection(None);
|
||||
assert_eq!(component.state, ComponentState::Missing);
|
||||
assert!(!component.installed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ignores_plain_proxifier_install() {
|
||||
let host = MockHost::new()
|
||||
@@ -61,6 +77,25 @@ fn env_override_can_point_to_portable_proxifyre_install() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reports_stopped_proxifyre_service_when_executable_exists() {
|
||||
let host = MockHost::new()
|
||||
.with_env("PROXYWARDEN_PROXIFYRE_ROOT", r"C:\Tools\ProxiFyre")
|
||||
.with_path(r"C:\Tools\ProxiFyre\ProxiFyre.exe")
|
||||
.with_stopped_service("ProxiFyreService");
|
||||
|
||||
let detected =
|
||||
detect_proxyfier_install_with_host(&host).expect("proxifyre executable should be detected");
|
||||
let component = proxyfier_component_from_detection(Some(&detected));
|
||||
|
||||
assert_eq!(component.state, ComponentState::Installed);
|
||||
assert!(component.installed);
|
||||
assert!(!component.running);
|
||||
assert_eq!(component.service_name, Some("ProxiFyreService".to_string()));
|
||||
assert_eq!(component.service_status, Some("stopped".to_string()));
|
||||
assert!(component.problems.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_proxyfier_returns_install_action_status() {
|
||||
let component = proxyfier_component_from_detection(None);
|
||||
@@ -73,7 +108,7 @@ fn missing_proxyfier_returns_install_action_status() {
|
||||
#[test]
|
||||
fn detects_running_local_singbox_from_default_install_root_and_service() {
|
||||
let host = MockHost::new()
|
||||
.with_path(r"C:\Program Files\ProxyWarden\sing-box\sing-box.exe")
|
||||
.with_path(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
|
||||
.with_service("ProxyWardenSingBox");
|
||||
|
||||
let detected =
|
||||
@@ -81,7 +116,7 @@ fn detects_running_local_singbox_from_default_install_root_and_service() {
|
||||
|
||||
assert_eq!(
|
||||
detected.executable_path,
|
||||
PathBuf::from(r"C:\Program Files\ProxyWarden\sing-box\sing-box.exe")
|
||||
PathBuf::from(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe")
|
||||
);
|
||||
assert_eq!(detected.service_name, "ProxyWardenSingBox");
|
||||
assert!(detected.running);
|
||||
@@ -92,7 +127,7 @@ fn detects_running_local_singbox_from_default_install_root_and_service() {
|
||||
assert!(component.running);
|
||||
assert_eq!(
|
||||
component.path,
|
||||
Some(r"C:\Program Files\ProxyWarden\sing-box\sing-box.exe".to_string())
|
||||
Some(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe".to_string())
|
||||
);
|
||||
assert!(component.problems.is_empty());
|
||||
}
|
||||
@@ -113,6 +148,11 @@ fn detects_stopped_local_singbox_from_env_override() {
|
||||
assert_eq!(component.state, ComponentState::Stopped);
|
||||
assert!(component.installed);
|
||||
assert!(!component.running);
|
||||
assert_eq!(
|
||||
component.service_name,
|
||||
Some("ProxyWardenSingBox".to_string())
|
||||
);
|
||||
assert_eq!(component.service_status, Some("stopped".to_string()));
|
||||
assert!(component
|
||||
.problems
|
||||
.iter()
|
||||
@@ -135,7 +175,7 @@ struct MockHost {
|
||||
env: HashMap<String, String>,
|
||||
paths: HashSet<String>,
|
||||
processes: HashSet<String>,
|
||||
services: HashSet<String>,
|
||||
services: HashMap<String, String>,
|
||||
registry: Vec<RegistryInstallEntry>,
|
||||
}
|
||||
|
||||
@@ -160,7 +200,14 @@ impl MockHost {
|
||||
}
|
||||
|
||||
fn with_service(mut self, service: &str) -> Self {
|
||||
self.services.insert(service.to_ascii_lowercase());
|
||||
self.services
|
||||
.insert(service.to_ascii_lowercase(), "running".to_string());
|
||||
self
|
||||
}
|
||||
|
||||
fn with_stopped_service(mut self, service: &str) -> Self {
|
||||
self.services
|
||||
.insert(service.to_ascii_lowercase(), "stopped".to_string());
|
||||
self
|
||||
}
|
||||
|
||||
@@ -188,8 +235,10 @@ impl ProxyfierDetectionHost for MockHost {
|
||||
self.processes.contains(&process_name.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
fn service_running(&self, service_name: &str) -> bool {
|
||||
self.services.contains(&service_name.to_ascii_lowercase())
|
||||
fn service_status(&self, service_name: &str) -> Option<String> {
|
||||
self.services
|
||||
.get(&service_name.to_ascii_lowercase())
|
||||
.cloned()
|
||||
}
|
||||
|
||||
fn registry_install_entries(&self) -> Vec<RegistryInstallEntry> {
|
||||
|
||||
@@ -184,6 +184,8 @@ fn missing_singbox_component() -> ComponentStatus {
|
||||
running: false,
|
||||
version: None,
|
||||
path: None,
|
||||
service_name: Some("ProxyWardenSingBox".to_string()),
|
||||
service_status: None,
|
||||
problems: vec!["Local sing-box is not installed".to_string()],
|
||||
actions: vec!["Install Local sing-box".to_string()],
|
||||
}
|
||||
@@ -197,7 +199,9 @@ fn running_singbox_component() -> ComponentStatus {
|
||||
installed: true,
|
||||
running: true,
|
||||
version: Some("1.11.0".to_string()),
|
||||
path: Some(r"C:\Tools\ProxyWarden\sing-box\sing-box.exe".to_string()),
|
||||
path: Some(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe".to_string()),
|
||||
service_name: Some("ProxyWardenSingBox".to_string()),
|
||||
service_status: Some("running".to_string()),
|
||||
problems: Vec::new(),
|
||||
actions: vec!["Restart".to_string(), "Stop".to_string()],
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ fn generates_selected_outbound_config_and_runs_check_when_binary_path_is_supplie
|
||||
let config = local_singbox_config("nl-1");
|
||||
let cache = subscription_cache();
|
||||
let checker = RecordingChecker::ok("configuration OK");
|
||||
let binary_path = Path::new(r"C:\Tools\ProxyWarden\sing-box\sing-box.exe");
|
||||
let binary_path = Path::new(r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe");
|
||||
|
||||
let generated = adapter
|
||||
.generate_config(
|
||||
@@ -211,7 +211,7 @@ fn local_singbox_config(selected_server_tag: &str) -> LocalSingBoxConfig {
|
||||
listen_host: "127.0.0.1".to_string(),
|
||||
listen_port: 1080,
|
||||
service_name: "ProxyWardenSingBox".to_string(),
|
||||
install_root: r"C:\Program Files\ProxyWarden\sing-box".to_string(),
|
||||
install_root: r"C:\Program Files\ProxyWarden\components\sing-box".to_string(),
|
||||
updated_at: Some("2026-07-07T10:00:00Z".to_string()),
|
||||
}
|
||||
}
|
||||
@@ -280,6 +280,8 @@ fn missing_singbox_component() -> ComponentStatus {
|
||||
running: false,
|
||||
version: None,
|
||||
path: None,
|
||||
service_name: Some("ProxyWardenSingBox".to_string()),
|
||||
service_status: None,
|
||||
problems: vec!["Local sing-box is not installed".to_string()],
|
||||
actions: vec!["Install Local sing-box".to_string()],
|
||||
}
|
||||
|
||||
@@ -47,10 +47,10 @@ noise
|
||||
|
||||
#[test]
|
||||
fn safe_install_dir_allows_only_proxywarden_singbox_folder() {
|
||||
assert!(
|
||||
ensure_safe_singbox_install_dir(Path::new(r"C:\Program Files\ProxyWarden\sing-box"))
|
||||
.is_ok()
|
||||
);
|
||||
assert!(ensure_safe_singbox_install_dir(Path::new(
|
||||
r"C:\Program Files\ProxyWarden\components\sing-box"
|
||||
))
|
||||
.is_ok());
|
||||
assert!(ensure_safe_singbox_install_dir(Path::new(r"C:\Windows")).is_err());
|
||||
assert!(ensure_safe_singbox_install_dir(Path::new(r"C:\Program Files\sing-box")).is_err());
|
||||
}
|
||||
@@ -72,7 +72,7 @@ fn service_control_script_targets_named_service_and_action() {
|
||||
#[test]
|
||||
fn service_control_script_syncs_generated_config_before_start() {
|
||||
let source = Path::new(r"C:\ProgramData\ProxyWarden\generated\sing-box-config.json");
|
||||
let target = Path::new(r"C:\Program Files\ProxyWarden\sing-box\config.json");
|
||||
let target = Path::new(r"C:\Program Files\ProxyWarden\components\sing-box\config.json");
|
||||
let script = service_control_script(
|
||||
SingBoxServiceAction::Start,
|
||||
"ProxyWardenSingBox",
|
||||
@@ -83,9 +83,9 @@ fn service_control_script_syncs_generated_config_before_start() {
|
||||
assert!(script.contains(
|
||||
"$configSource = 'C:\\ProgramData\\ProxyWarden\\generated\\sing-box-config.json'"
|
||||
));
|
||||
assert!(
|
||||
script.contains("$configTarget = 'C:\\Program Files\\ProxyWarden\\sing-box\\config.json'")
|
||||
);
|
||||
assert!(script.contains(
|
||||
"$configTarget = 'C:\\Program Files\\ProxyWarden\\components\\sing-box\\config.json'"
|
||||
));
|
||||
assert!(script.contains("Copy-Item -LiteralPath $configSource"));
|
||||
assert!(script.contains("'config_sync_failed'"));
|
||||
}
|
||||
@@ -116,10 +116,12 @@ fn install_singbox_script_parses_as_powershell() {
|
||||
|
||||
fn detected_singbox(binary_exists: bool, wrapper_exists: bool, running: bool) -> DetectedSingBox {
|
||||
DetectedSingBox {
|
||||
install_dir: PathBuf::from(r"C:\Program Files\ProxyWarden\sing-box"),
|
||||
executable_path: PathBuf::from(r"C:\Program Files\ProxyWarden\sing-box\sing-box.exe"),
|
||||
install_dir: PathBuf::from(r"C:\Program Files\ProxyWarden\components\sing-box"),
|
||||
executable_path: PathBuf::from(
|
||||
r"C:\Program Files\ProxyWarden\components\sing-box\sing-box.exe",
|
||||
),
|
||||
wrapper_path: PathBuf::from(
|
||||
r"C:\Program Files\ProxyWarden\sing-box\ProxyWardenSingBox.exe",
|
||||
r"C:\Program Files\ProxyWarden\components\sing-box\ProxyWardenSingBox.exe",
|
||||
),
|
||||
binary_exists,
|
||||
wrapper_exists,
|
||||
|
||||
@@ -57,7 +57,7 @@ fn roundtrips_local_singbox_config_and_subscription_cache() {
|
||||
listen_host: "127.0.0.1".to_string(),
|
||||
listen_port: 1080,
|
||||
service_name: "ProxyWardenSingBox".to_string(),
|
||||
install_root: r"C:\Program Files\ProxyWarden\sing-box".to_string(),
|
||||
install_root: r"C:\Program Files\ProxyWarden\components\sing-box".to_string(),
|
||||
updated_at: Some("2026-07-07T10:00:00Z".to_string()),
|
||||
};
|
||||
let cache = sample_subscription_cache();
|
||||
@@ -370,6 +370,8 @@ fn sample_component() -> ComponentStatus {
|
||||
running: false,
|
||||
version: None,
|
||||
path: None,
|
||||
service_name: Some("ProxiFyreService".to_string()),
|
||||
service_status: None,
|
||||
problems: vec!["ProxiFyre не установлен".to_string()],
|
||||
actions: vec!["Установить ProxiFyre".to_string()],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user