@@ -28,9 +28,17 @@ use serde::{Deserialize, Serialize};
use std ::env ;
use std ::fs ;
use std ::path ::{ Path , PathBuf } ;
use std ::process ::Command ;
use std ::process ::{ Command , Output } ;
use std ::time ::{ SystemTime , UNIX_EPOCH } ;
const PROXIFYRE_INSTALL_DIR : & str = r "C:\Tools\ProxiFyre" ;
const PROXIFYRE_RELEASE_API_URL : & str =
" https://api.github.com/repos/wiresock/proxifyre/releases/latest " ;
const NDISAPI_RELEASE_API_URL : & str =
" https://api.github.com/repos/wiresock/ndisapi/releases/latest " ;
const VC_REDIST_X64_URL : & str = " https://aka.ms/vc14/vc_redist.x64.exe " ;
const VC_REDIST_X86_URL : & str = " https://aka.ms/vc14/vc_redist.x86.exe " ;
#[ derive(Debug, Clone) ]
pub struct CommandState {
root : PathBuf ,
@@ -110,6 +118,24 @@ pub struct SavedStateResponse {
pub generated_config_path : String ,
}
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
pub struct ProxiFyreSetupStatusDto {
pub ready : bool ,
pub missing_count : usize ,
pub items : Vec < ProxiFyreSetupItemDto > ,
}
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
pub struct ProxiFyreSetupItemDto {
pub id : String ,
pub name : String ,
pub installed : bool ,
pub version : Option < String > ,
pub details : String ,
}
#[ derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
pub struct ProfileInputDto {
@@ -384,6 +410,13 @@ pub async fn get_components(
. map_err ( background_task_error ) ?
}
#[ tauri::command ]
pub async fn get_proxifyre_setup_status ( ) -> Result < ProxiFyreSetupStatusDto , CommandError > {
tauri ::async_runtime ::spawn_blocking ( build_proxifyre_setup_status )
. await
. map_err ( background_task_error )
}
#[ tauri::command ]
pub fn resolve_profile_preview (
input : ProfileInputDto ,
@@ -444,6 +477,23 @@ pub async fn stop_proxifyre_service() -> Result<ComponentStatusDto, CommandError
. map_err ( background_task_error ) ?
}
#[ tauri::command ]
pub async fn install_proxifyre (
state : tauri ::State < '_ , CommandState > ,
) -> Result < ComponentStatusDto , CommandError > {
let storage = state . storage ( ) ;
tauri ::async_runtime ::spawn_blocking ( move | | install_proxifyre_component ( & storage ) )
. await
. map_err ( background_task_error ) ?
}
#[ tauri::command ]
pub async fn uninstall_proxifyre ( ) -> Result < ComponentStatusDto , CommandError > {
tauri ::async_runtime ::spawn_blocking ( uninstall_proxifyre_component )
. await
. map_err ( background_task_error ) ?
}
pub fn build_status ( storage : & JsonStorage ) -> Result < StatusResponse , CommandError > {
let profiles = storage . read_profiles ( ) . map_err ( storage_error ) ? ;
let targets = storage . read_targets ( ) . map_err ( storage_error ) ? ;
@@ -1063,7 +1113,7 @@ fn write_elevated_service_script(
let script_path = env ::temp_dir ( ) . join ( format! ( " vpn-proxy-proxifyre-service- {nonce} .ps1 " ) ) ;
let script = elevated_service_script ( action , service_names ) ;
fs ::write ( & script_path , script ) . map_err ( | error | {
write_powershell_script ( & script_path , & script ) . map_err ( | error | {
CommandError ::new (
action . error_code ( ) ,
format! (
@@ -1161,6 +1211,640 @@ exit 4
)
}
#[ derive(Debug, Clone, Copy, PartialEq, Eq) ]
enum ProxiFyrePackageAction {
Install ,
Uninstall ,
}
impl ProxiFyrePackageAction {
fn error_code ( self ) -> & 'static str {
match self {
ProxiFyrePackageAction ::Install = > " proxifyre_install_failed " ,
ProxiFyrePackageAction ::Uninstall = > " proxifyre_uninstall_failed " ,
}
}
fn label ( self ) -> & 'static str {
match self {
ProxiFyrePackageAction ::Install = > " установить " ,
ProxiFyrePackageAction ::Uninstall = > " удалить " ,
}
}
fn file_label ( self ) -> & 'static str {
match self {
ProxiFyrePackageAction ::Install = > " install " ,
ProxiFyrePackageAction ::Uninstall = > " uninstall " ,
}
}
}
fn install_proxifyre_component ( storage : & JsonStorage ) -> Result < ComponentStatusDto , CommandError > {
let generated_config_path = storage
. paths ( )
. generated_dir
. join ( " proxifyre-app-config.json " ) ;
let script = install_proxifyre_script ( & generated_config_path ) ;
run_elevated_package_script (
ProxiFyrePackageAction ::Install ,
script ,
& storage . paths ( ) . state_dir ,
) ? ;
let refreshed = detect_proxyfier_install ( ) ;
let Some ( detected ) = refreshed . as_ref ( ) else {
return Err ( CommandError ::new (
ProxiFyrePackageAction ::Install . error_code ( ) ,
" Установка ProxiFyre завершилась, но приложение не найдено после проверки. " ,
) ) ;
} ;
Ok ( ComponentStatusDto ::from (
& proxyfier_component_from_detection ( Some ( detected ) ) ,
) )
}
fn uninstall_proxifyre_component ( ) -> Result < ComponentStatusDto , CommandError > {
let Some ( detected ) = detect_proxyfier_install ( ) else {
let component = proxyfier_component_from_detection ( None ) ;
return Ok ( ComponentStatusDto ::from ( & component ) ) ;
} ;
ensure_safe_proxifyre_install_dir ( & detected . install_dir ) ? ;
let script = uninstall_proxifyre_script ( & detected . install_dir , & detected . executable_path ) ;
let artifact_dir = default_config_root ( ) . join ( " state " ) ;
run_elevated_package_script ( ProxiFyrePackageAction ::Uninstall , script , & artifact_dir ) ? ;
let refreshed = detect_proxyfier_install ( ) ;
if refreshed . is_some ( ) {
return Err ( CommandError ::new (
ProxiFyrePackageAction ::Uninstall . error_code ( ) ,
" Удаление ProxiFyre завершилось, но приложение все еще найдено на компьютере. " ,
) ) ;
}
let component = proxyfier_component_from_detection ( None ) ;
Ok ( ComponentStatusDto ::from ( & component ) )
}
fn build_proxifyre_setup_status ( ) -> ProxiFyreSetupStatusDto {
let vc_runtime = detect_vc_runtime ( ) ;
let packet_filter = detect_windows_packet_filter ( ) ;
let proxifyre = detect_proxyfier_install ( ) ;
let vc_runtime_item = setup_item_from_program (
" vc-runtime " ,
& format! ( " Microsoft Visual C++ Runtime ( {} ) " , runtime_arch_label ( ) ) ,
vc_runtime ,
" Нужен для запуска ProxiFyre.exe. Установщик скачает официальный vc_redist от Microsoft. " ,
) ;
let packet_filter_item = setup_item_from_program (
" packet-filter " ,
" Windows Packet Filter " ,
packet_filter ,
" Сетевой драйвер NT Kernel/WireSock, через который ProxiFyre перехватывает трафик приложений. " ,
) ;
let proxifyre_item = match proxifyre {
Some ( detected ) = > ProxiFyreSetupItemDto {
id : " proxifyre " . to_string ( ) ,
name : " ProxiFyre " . to_string ( ) ,
installed : true ,
version : Some ( if detected . running {
" служба запущена " . to_string ( )
} else {
" служба не запущена " . to_string ( )
} ) ,
details : detected . install_dir . display ( ) . to_string ( ) ,
} ,
None = > ProxiFyreSetupItemDto {
id : " proxifyre " . to_string ( ) ,
name : " ProxiFyre " . to_string ( ) ,
installed : false ,
version : None ,
details : format ! (
" Будет скачан с GitHub и распакован в {}. " ,
PROXIFYRE_INSTALL_DIR
) ,
} ,
} ;
let items = vec! [ vc_runtime_item , packet_filter_item , proxifyre_item ] ;
let missing_count = items . iter ( ) . filter ( | item | ! item . installed ) . count ( ) ;
ProxiFyreSetupStatusDto {
ready : missing_count = = 0 ,
missing_count ,
items ,
}
}
fn setup_item_from_program (
id : & str ,
name : & str ,
program : Option < InstalledProgram > ,
missing_details : & str ,
) -> ProxiFyreSetupItemDto {
match program {
Some ( program ) = > ProxiFyreSetupItemDto {
id : id . to_string ( ) ,
name : name . to_string ( ) ,
installed : true ,
version : program . display_version ,
details : program . display_name ,
} ,
None = > ProxiFyreSetupItemDto {
id : id . to_string ( ) ,
name : name . to_string ( ) ,
installed : false ,
version : None ,
details : missing_details . to_string ( ) ,
} ,
}
}
#[ derive(Debug, Clone) ]
struct InstalledProgram {
display_name : String ,
display_version : Option < String > ,
}
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " PascalCase " ) ]
struct InstalledProgramJson {
display_name : Option < String > ,
display_version : Option < String > ,
}
fn detect_vc_runtime ( ) -> Option < InstalledProgram > {
installed_program ( & vc_runtime_registry_pattern ( ) )
}
fn detect_windows_packet_filter ( ) -> Option < InstalledProgram > {
installed_program ( " Windows Packet Filter|WinpkFilter|NDISAPI " )
}
fn installed_program ( pattern : & str ) -> Option < InstalledProgram > {
let script = format! (
r # "
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
$program = Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
Where-Object {{ $_.DisplayName -match '{}' }} |
Select-Object -First 1 DisplayName, DisplayVersion
if ($null -ne $program) {{
$program | ConvertTo-Json -Compress
}}
"# ,
escape_powershell_single ( pattern )
) ;
let output = Command ::new ( " powershell " )
. args ( [
" -NoProfile " ,
" -NonInteractive " ,
" -ExecutionPolicy " ,
" Bypass " ,
" -Command " ,
script . as_str ( ) ,
] )
. output ( )
. ok ( ) ? ;
if ! output . status . success ( ) {
return None ;
}
let stdout = String ::from_utf8_lossy ( & output . stdout ) ;
let payload = stdout . trim ( ) ;
if payload . is_empty ( ) | | payload . eq_ignore_ascii_case ( " null " ) {
return None ;
}
let parsed : InstalledProgramJson = serde_json ::from_str ( payload ) . ok ( ) ? ;
let display_name = parsed . display_name ? . trim ( ) . to_string ( ) ;
if display_name . is_empty ( ) {
return None ;
}
Some ( InstalledProgram {
display_name ,
display_version : parsed
. display_version
. map ( | version | version . trim ( ) . to_string ( ) )
. filter ( | version | ! version . is_empty ( ) ) ,
} )
}
fn vc_runtime_registry_pattern ( ) -> String {
let arch = runtime_arch_label ( ) ;
if arch = = " ARM64 " {
return r "Microsoft Visual C\+\+.*Redistributable.*\((ARM64|x64)\)" . to_string ( ) ;
}
format! ( r "Microsoft Visual C\+\+.*Redistributable.*\({arch}\)" )
}
fn runtime_arch_label ( ) -> & 'static str {
if cfg! ( target_arch = " aarch64 " ) {
" ARM64 "
} else if cfg! ( target_arch = " x86 " ) {
" x86 "
} else {
" x64 "
}
}
fn run_elevated_package_script (
action : ProxiFyrePackageAction ,
body : String ,
artifact_dir : & Path ,
) -> Result < ( ) , CommandError > {
let nonce = SystemTime ::now ( )
. duration_since ( UNIX_EPOCH )
. map ( | duration | duration . as_millis ( ) )
. unwrap_or ( 0 ) ;
fs ::create_dir_all ( artifact_dir ) . map_err ( | error | {
CommandError ::new (
action . error_code ( ) ,
format! (
" Н е удалось создать папку для временных файлов ProxiFyre '{} ': {error} " ,
artifact_dir . display ( )
) ,
)
} ) ? ;
let script_path = artifact_dir . join ( format! (
" vpn-proxy-proxifyre- {} - {nonce} .ps1 " ,
action . file_label ( )
) ) ;
let result_path = artifact_dir . join ( format! (
" vpn-proxy-proxifyre- {} - {nonce} .log " ,
action . file_label ( )
) ) ;
let script = wrap_elevated_package_script ( & body , & result_path ) ;
write_powershell_script ( & script_path , & script ) . map_err ( | error | {
CommandError ::new (
action . error_code ( ) ,
format! (
" Н е удалось подготовить временный скрипт, чтобы {} ProxiFyre ' {} ': {error} " ,
action . label ( ) ,
script_path . display ( )
) ,
)
} ) ? ;
let launch_script = format! (
r # "
$ErrorActionPreference = 'Stop'
$resultPath = '{}'
try {{
$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}')
if ($null -eq $p) {{
Set-Content -LiteralPath $resultPath -Value 'Elevated PowerShell не был запущен.' -Encoding UTF8
exit 1
}}
exit $p.ExitCode
}} catch {{
Set-Content -LiteralPath $resultPath -Value ($_ | Out-String) -Encoding UTF8
exit 1
}}
"# ,
escape_powershell_single ( & result_path . display ( ) . to_string ( ) ) ,
escape_powershell_single ( & script_path . display ( ) . to_string ( ) )
) ;
let output = Command ::new ( " powershell " )
. args ( [
" -NoProfile " ,
" -NonInteractive " ,
" -ExecutionPolicy " ,
" Bypass " ,
" -Command " ,
launch_script . as_str ( ) ,
] )
. output ( ) ;
let _ = fs ::remove_file ( & script_path ) ;
match output {
Ok ( output ) if output . status . success ( ) = > {
let _ = fs ::remove_file ( & result_path ) ;
Ok ( ( ) )
}
Ok ( output ) = > {
let details = package_failure_details ( & result_path , & output ) ;
let _ = fs ::remove_file ( & result_path ) ;
Err ( CommandError ::new (
action . error_code ( ) ,
format! (
" Н е удалось {} ProxiFyre. Код elevated-команды: {} . {details} " ,
action . label ( ) ,
output . status . code ( ) . unwrap_or ( - 1 ) ,
) ,
) )
}
Err ( error ) = > Err ( CommandError ::new (
action . error_code ( ) ,
format! (
" Н е удалось запросить права администратора, чтобы {} ProxiFyre: {error} " ,
action . label ( )
) ,
) ) ,
}
}
pub ( crate ) fn wrap_elevated_package_script ( body : & str , result_path : & Path ) -> String {
let mut script = String ::new ( ) ;
script . push_str ( " $ErrorActionPreference = 'Stop' \n " ) ;
script . push_str ( & format! (
" $resultPath = ' {} ' \n " ,
escape_powershell_single ( & result_path . display ( ) . to_string ( ) )
) ) ;
script . push_str ( " try { \n " ) ;
script . push_str ( body ) ;
script . push_str (
r # "
Set-Content -LiteralPath $resultPath -Value 'ok' -Encoding UTF8
exit 0
} catch {
$message = ($_ | Out-String)
Set-Content -LiteralPath $resultPath -Value $message -Encoding UTF8
exit 1
}
"# ,
) ;
script
}
fn write_powershell_script ( path : & Path , script : & str ) -> std ::io ::Result < ( ) > {
let mut bytes = Vec ::with_capacity ( script . len ( ) + 3 ) ;
bytes . extend_from_slice ( & [ 0xEF , 0xBB , 0xBF ] ) ;
bytes . extend_from_slice ( script . as_bytes ( ) ) ;
fs ::write ( path , bytes )
}
pub ( crate ) fn install_proxifyre_script ( generated_config_path : & Path ) -> String {
let mut script = String ::new ( ) ;
script . push_str ( & format! (
" $targetDir = ' {} ' \n " ,
escape_powershell_single ( PROXIFYRE_INSTALL_DIR )
) ) ;
script . push_str ( & format! (
" $generatedConfigPath = ' {} ' \n " ,
escape_powershell_single ( & generated_config_path . display ( ) . to_string ( ) )
) ) ;
script . push_str ( & format! (
" $proxifyreReleaseApi = ' {} ' \n " ,
escape_powershell_single ( PROXIFYRE_RELEASE_API_URL )
) ) ;
script . push_str ( & format! (
" $ndisapiReleaseApi = ' {} ' \n " ,
escape_powershell_single ( NDISAPI_RELEASE_API_URL )
) ) ;
script . push_str ( & format! (
" $vcRedistX64Url = ' {} ' \n " ,
escape_powershell_single ( VC_REDIST_X64_URL )
) ) ;
script . push_str ( & format! (
" $vcRedistX86Url = ' {} ' \n " ,
escape_powershell_single ( VC_REDIST_X86_URL )
) ) ;
script . push_str (
r # "
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
function Get-NativeArchitecture {
$processor = Get-CimInstance Win32_Processor | Select-Object -First 1
if ($null -ne $processor -and $processor.Architecture -eq 12) { return 'ARM64' }
if ([Environment]::Is64BitOperatingSystem) { return 'x64' }
return 'x86'
}
function Invoke-Download([string]$uri, [string]$path) {
Invoke-WebRequest -UseBasicParsing -Uri $uri -OutFile $path -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' }
}
function Select-Asset($assets, [string]$pattern, [string]$label) {
$asset = $assets | Where-Object { $_.name -match $pattern } | Select-Object -First 1
if ($null -eq $asset) { throw "Н е найден подходящий asset для $label ($pattern)." }
return $asset
}
function Verify-AssetHash([string]$path, $asset) {
if ($asset.digest -match '^sha256:(.+)$') {
$expected = $Matches[1].ToLowerInvariant()
$actual = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash.ToLowerInvariant()
if ($actual -ne $expected) {
throw "SHA256 не совпал для $($asset.name). Ожидалось $expected, получилось $actual."
}
}
}
function Assert-ExitCode($process, [string]$label) {
if ($process.ExitCode -ne 0 -and $process.ExitCode -ne 3010) {
throw "$label завершился с кодом $($process.ExitCode)."
}
}
function Get-InstalledProgram([string]$pattern) {
$paths = @(
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
return Get-ItemProperty -Path $paths -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -match $pattern } |
Select-Object -First 1
}
function Test-VcRuntime([string]$arch) {
$pattern = if ($arch -eq 'ARM64') {
'Microsoft Visual C\+\+.*Redistributable.*\((ARM64|x64)\)'
} else {
"Microsoft Visual C\+\+.*Redistributable.*\($arch\)"
}
return $null -ne (Get-InstalledProgram $pattern)
}
function Test-WindowsPacketFilter {
return $null -ne (Get-InstalledProgram 'Windows Packet Filter|WinpkFilter|NDISAPI')
}
function Get-LogTail([string]$path) {
if (-not (Test-Path -LiteralPath $path)) { return '' }
return (Get-Content -LiteralPath $path -Tail 40 -ErrorAction SilentlyContinue) -join ' '
}
$arch = Get-NativeArchitecture
$workDir = Join-Path ([IO.Path]::GetTempPath()) 'vpn-proxy-proxifyre-install'
$extractDir = Join-Path $workDir 'proxifyre'
Remove-Item -LiteralPath $workDir -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $workDir, $extractDir, $targetDir | Out-Null
if (-not (Test-VcRuntime $arch)) {
$vcRedistPath = Join-Path $workDir 'vc_redist.exe'
$vcRedistUrl = if ($arch -eq 'x86') { $vcRedistX86Url } else { $vcRedistX64Url }
Invoke-Download $vcRedistUrl $vcRedistPath
$vcProcess = Start-Process -FilePath $vcRedistPath -ArgumentList @('/install', '/quiet', '/norestart') -Wait -PassThru -WindowStyle Hidden
if ($vcProcess.ExitCode -ne 0 -and $vcProcess.ExitCode -ne 3010 -and $vcProcess.ExitCode -ne 1638 -and -not (Test-VcRuntime $arch)) {
throw "Visual C++ Runtime завершился с кодом $($vcProcess.ExitCode)."
}
}
if (-not (Test-WindowsPacketFilter)) {
$ndisRelease = Invoke-RestMethod -Uri $ndisapiReleaseApi -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' }
$ndisPattern = if ($arch -eq 'ARM64') { 'ARM64\.msi$' } elseif ($arch -eq 'x86') { 'x86\.msi$' } else { 'x64\.msi$' }
$ndisAsset = Select-Asset $ndisRelease.assets $ndisPattern 'Windows Packet Filter'
$ndisPath = Join-Path $workDir $ndisAsset.name
$ndisLogPath = Join-Path $workDir 'windows-packet-filter-install.log'
Invoke-Download $ndisAsset.browser_download_url $ndisPath
Verify-AssetHash $ndisPath $ndisAsset
$ndisProcess = Start-Process -FilePath 'msiexec.exe' -ArgumentList @('/i', $ndisPath, '/qn', '/norestart', '/L*v', $ndisLogPath) -Wait -PassThru -WindowStyle Hidden
if ($ndisProcess.ExitCode -ne 0 -and $ndisProcess.ExitCode -ne 3010 -and -not (Test-WindowsPacketFilter)) {
$ndisLogTail = Get-LogTail $ndisLogPath
throw "Windows Packet Filter завершился с кодом $($ndisProcess.ExitCode). MSI log: $ndisLogPath $ndisLogTail"
}
}
$proxifyreRelease = Invoke-RestMethod -Uri $proxifyreReleaseApi -Headers @{ 'User-Agent' = 'vpn-proxy-windows-client' }
$proxifyrePattern = if ($arch -eq 'ARM64') { 'ARM64-signed\.zip$' } elseif ($arch -eq 'x86') { 'x86-signed\.zip$' } else { 'x64-signed\.zip$' }
$proxifyreAsset = Select-Asset $proxifyreRelease.assets $proxifyrePattern 'ProxiFyre'
$proxifyreZipPath = Join-Path $workDir $proxifyreAsset.name
Invoke-Download $proxifyreAsset.browser_download_url $proxifyreZipPath
Verify-AssetHash $proxifyreZipPath $proxifyreAsset
Expand-Archive -LiteralPath $proxifyreZipPath -DestinationPath $extractDir -Force
$proxifyreExe = Get-ChildItem -LiteralPath $extractDir -Recurse -Filter 'ProxiFyre.exe' | Select-Object -First 1
if ($null -eq $proxifyreExe) { throw 'В архиве ProxiFyre не найден ProxiFyre.exe.' }
Copy-Item -Path (Join-Path $proxifyreExe.Directory.FullName '*') -Destination $targetDir -Recurse -Force
$configTarget = Join-Path $targetDir 'app-config.json'
if (Test-Path -LiteralPath $generatedConfigPath) {
Copy-Item -LiteralPath $generatedConfigPath -Destination $configTarget -Force
} elseif (-not (Test-Path -LiteralPath $configTarget)) {
$emptyConfig = '{"logLevel":"Info","bypassLan":true,"proxies":[]}'
Set-Content -LiteralPath $configTarget -Value $emptyConfig -Encoding UTF8
}
Push-Location $targetDir
try {
& .\ProxiFyre.exe stop | Out-Null
& .\ProxiFyre.exe uninstall | Out-Null
& .\ProxiFyre.exe install
if ($LASTEXITCODE -ne 0) { throw "ProxiFyre.exe install завершился с кодом $LASTEXITCODE." }
& .\ProxiFyre.exe start
if ($LASTEXITCODE -ne 0) {
Start-Service -Name 'ProxiFyreService' -ErrorAction Stop
}
} finally {
Pop-Location
}
"# ,
) ;
script
}
fn uninstall_proxifyre_script ( install_dir : & Path , executable_path : & Path ) -> String {
let mut script = String ::new ( ) ;
script . push_str ( & format! (
" $installDir = ' {} ' \n " ,
escape_powershell_single ( & install_dir . display ( ) . to_string ( ) )
) ) ;
script . push_str ( & format! (
" $exePath = ' {} ' \n " ,
escape_powershell_single ( & executable_path . display ( ) . to_string ( ) )
) ) ;
script . push_str (
r # "
function Find-ProxiFyreService {
foreach ($name in @('ProxiFyreService', 'ProxiFyre')) {
$candidate = Get-Service -Name $name -ErrorAction SilentlyContinue
if ($null -ne $candidate) { return $candidate }
}
return Get-Service |
Where-Object { $_.Name -match 'ProxiFyre|Proxifyre' -or $_.DisplayName -match 'ProxiFyre|Proxifyre' } |
Select-Object -First 1
}
function Get-ServiceProcessId([string]$name) {
$escapedName = $name.Replace("'", "''")
$record = Get-CimInstance Win32_Service -Filter "Name='$escapedName'" -ErrorAction SilentlyContinue
if ($null -eq $record) { return 0 }
return [int]$record.ProcessId
}
$service = Find-ProxiFyreService
if ($null -ne $service -and $service.Status -ne 'Stopped') {
try {
if ($service.CanStop) { Stop-Service -Name $service.Name -Force -ErrorAction SilentlyContinue }
$service = Get-Service -Name $service.Name -ErrorAction SilentlyContinue
if ($null -ne $service) { $service.WaitForStatus('Stopped', [TimeSpan]::FromSeconds(8)) }
} catch {}
}
$service = Find-ProxiFyreService
if ($null -ne $service -and $service.Status -ne 'Stopped') {
$processId = Get-ServiceProcessId $service.Name
if ($processId -gt 0) {
taskkill.exe /PID $processId /F | Out-Null
Start-Sleep -Milliseconds 700
}
}
if (Test-Path -LiteralPath $exePath) {
Push-Location (Split-Path -Parent $exePath)
try {
& $exePath uninstall | Out-Null
} finally {
Pop-Location
}
}
$service = Find-ProxiFyreService
if ($null -ne $service) {
sc.exe delete $service.Name | Out-Null
}
Get-Process -Name 'ProxiFyre' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
if (Test-Path -LiteralPath $installDir) {
Remove-Item -LiteralPath $installDir -Recurse -Force
}
"# ,
) ;
script
}
fn ensure_safe_proxifyre_install_dir ( path : & Path ) -> Result < ( ) , CommandError > {
let name = path
. file_name ( )
. and_then ( | value | value . to_str ( ) )
. map ( | value | value . to_ascii_lowercase ( ) )
. unwrap_or_default ( ) ;
if path . parent ( ) . is_some ( ) & & name . contains ( " proxifyre " ) {
return Ok ( ( ) ) ;
}
Err ( CommandError ::new (
ProxiFyrePackageAction ::Uninstall . error_code ( ) ,
format! (
" Отказываюсь рекурсивно удалять папку ProxiFyre с небезопасным путем: {} " ,
path . display ( )
) ,
) )
}
fn apply_to_detected_proxyfier (
request : HelperApplyRequest < '_ > ,
detected : & DetectedProxyfier ,
@@ -1280,6 +1964,55 @@ fn background_task_error(error: impl std::fmt::Display) -> CommandError {
)
}
fn package_failure_details ( result_path : & Path , output : & Output ) -> String {
let mut parts = Vec ::new ( ) ;
if let Ok ( contents ) = fs ::read_to_string ( result_path ) {
let details = compact_error_text ( & contents ) ;
if ! details . is_empty ( ) & & ! details . eq_ignore_ascii_case ( " ok " ) {
parts . push ( details ) ;
}
}
let stdout = String ::from_utf8_lossy ( & output . stdout ) ;
let stdout = compact_error_text ( & stdout ) ;
if ! stdout . is_empty ( ) {
parts . push ( format! ( " stdout: {stdout} " ) ) ;
}
let stderr = String ::from_utf8_lossy ( & output . stderr ) ;
let stderr = compact_error_text ( & stderr ) ;
if ! stderr . is_empty ( ) {
parts . push ( format! ( " stderr: {stderr} " ) ) ;
}
if parts . is_empty ( ) {
parts . push (
" Лог elevated-скрипта не создан. Обычно это значит, что окно UAC было отменено или Windows не дала запустить elevated PowerShell. "
. to_string ( ) ,
) ;
}
parts . join ( " " )
}
fn compact_error_text ( value : & str ) -> String {
let text = value
. lines ( )
. map ( str ::trim )
. filter ( | line | ! line . is_empty ( ) )
. collect ::< Vec < _ > > ( )
. join ( " " ) ;
let max_chars = 1400 ;
if text . chars ( ) . count ( ) < = max_chars {
return text ;
}
let truncated = text . chars ( ) . take ( max_chars ) . collect ::< String > ( ) ;
format! ( " {truncated} ... " )
}
fn parse_service_command_output ( stdout : & [ u8 ] ) -> Option < ServiceCommandOutput > {
let stdout = String ::from_utf8_lossy ( stdout ) ;
let payload = stdout