Bump release version and snapshot startup state

This commit is contained in:
2026-07-08 14:56:38 +03:00
parent 7bb437f28b
commit e3d3a0bb47
18 changed files with 149 additions and 72 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "proxywarden", "name": "proxywarden",
"version": "0.1.0", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "proxywarden", "name": "proxywarden",
"version": "0.1.0", "version": "1.0.0",
"dependencies": { "dependencies": {
"@fontsource-variable/jetbrains-mono": "^5.2.8", "@fontsource-variable/jetbrains-mono": "^5.2.8",
"@tauri-apps/api": "^2.0.0", "@tauri-apps/api": "^2.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "proxywarden", "name": "proxywarden",
"version": "0.1.0", "version": "1.0.0",
"private": true, "private": true,
"type": "module", "type": "module",
"description": "Standalone Windows desktop proxy management app for ProxyWarden.", "description": "Standalone Windows desktop proxy management app for ProxyWarden.",

View File

@@ -353,13 +353,12 @@ function New-ReleaseDirectory {
$releaseDir = Join-Path $root "proxywarden-v$TargetVersion" $releaseDir = Join-Path $root "proxywarden-v$TargetVersion"
if ((Test-Path -LiteralPath $releaseDir) -and $Force) { if (Test-Path -LiteralPath $releaseDir) {
if (-not (Test-IsSubPath -Parent $root -Child $releaseDir)) { if (-not (Test-IsSubPath -Parent $root -Child $releaseDir)) {
throw "Refusing to remove release directory outside OutputRoot: $releaseDir" throw "Refusing to remove release directory outside OutputRoot: $releaseDir"
} }
Write-Host "Replacing existing release directory: $releaseDir"
Remove-Item -LiteralPath $releaseDir -Recurse -Force Remove-Item -LiteralPath $releaseDir -Recurse -Force
} elseif (Test-Path -LiteralPath $releaseDir) {
throw "Release directory already exists: $releaseDir. Use -Force to replace it."
} }
New-Item -ItemType Directory -Path (Join-Path $releaseDir "artifacts") -Force | Out-Null New-Item -ItemType Directory -Path (Join-Path $releaseDir "artifacts") -Force | Out-Null
@@ -409,10 +408,6 @@ function Invoke-ReleaseBuild {
function Copy-ReleaseArtifacts { function Copy-ReleaseArtifacts {
param([string]$ReleaseDir) param([string]$ReleaseDir)
if ($SkipBuild) {
return @()
}
if (-not (Test-Path -LiteralPath $BundleRoot)) { if (-not (Test-Path -LiteralPath $BundleRoot)) {
throw "Tauri bundle output was not found: $BundleRoot" throw "Tauri bundle output was not found: $BundleRoot"
} }

2
src-tauri/Cargo.lock generated
View File

@@ -2314,7 +2314,7 @@ dependencies = [
[[package]] [[package]]
name = "proxywarden" name = "proxywarden"
version = "0.1.0" version = "1.0.0"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"reqwest 0.12.28", "reqwest 0.12.28",

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "proxywarden" name = "proxywarden"
version = "0.1.0" version = "1.0.0"
description = "Standalone Windows desktop proxy management app for ProxyWarden." description = "Standalone Windows desktop proxy management app for ProxyWarden."
authors = ["ProxyWarden"] authors = ["ProxyWarden"]
edition = "2021" edition = "2021"

View File

@@ -1,10 +1,10 @@
use crate::models::{LocalSingBoxConfig, SubscriptionCache}; use crate::models::{LocalSingBoxConfig, SubscriptionCache};
use crate::process::command_no_window;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::{ use std::{
env, fs, env, fs,
path::Path, path::Path,
process::Command,
time::{SystemTime, UNIX_EPOCH}, time::{SystemTime, UNIX_EPOCH},
}; };
@@ -215,7 +215,7 @@ impl SingBoxConfigChecker for SingBoxCommandChecker {
) )
})?; })?;
let output = Command::new(binary_path) let output = command_no_window(binary_path)
.arg("check") .arg("check")
.arg("-c") .arg("-c")
.arg(&config_path) .arg(&config_path)

View File

@@ -20,6 +20,7 @@ use crate::models::{
Profile, ProfileInput, ProfileItem, ProfileItemInput, ProfileItemType, Protocol, ProxyProtocol, Profile, ProfileInput, ProfileItem, ProfileItemInput, ProfileItemType, Protocol, ProxyProtocol,
SubscriptionCache, SubscriptionServer, Target, TargetInput, TargetKind, SubscriptionCache, SubscriptionServer, Target, TargetInput, TargetKind,
}; };
use crate::process::command_no_window;
#[cfg(test)] #[cfg(test)]
use crate::proxifyre::{ProxiFyreAdapter, ProxiFyreConfig, ProxiFyreProxy}; use crate::proxifyre::{ProxiFyreAdapter, ProxiFyreConfig, ProxiFyreProxy};
#[cfg(test)] #[cfg(test)]
@@ -185,6 +186,17 @@ pub struct SavedStateResponse {
pub generated_config_path: String, pub generated_config_path: String,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct StartupSnapshotResponse {
pub admin_status: AdminStatusResponse,
pub saved_state: SavedStateResponse,
pub components: Vec<ComponentStatusDto>,
pub proxifyre_setup_status: ProxiFyreSetupStatusDto,
pub singbox_status: LocalSingBoxStatusResponse,
pub singbox_setup_status: SingBoxSetupStatusDto,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ProxiFyreSetupStatusDto { pub struct ProxiFyreSetupStatusDto {
@@ -563,6 +575,16 @@ pub fn restart_as_admin(app: tauri::AppHandle) -> Result<(), CommandError> {
Ok(()) Ok(())
} }
#[tauri::command]
pub async fn get_startup_snapshot(
state: tauri::State<'_, CommandState>,
) -> Result<StartupSnapshotResponse, CommandError> {
let storage = state.storage();
tauri::async_runtime::spawn_blocking(move || read_startup_snapshot(&storage))
.await
.map_err(background_task_error)?
}
#[tauri::command] #[tauri::command]
pub fn get_saved_state( pub fn get_saved_state(
state: tauri::State<'_, CommandState>, state: tauri::State<'_, CommandState>,
@@ -1011,6 +1033,41 @@ pub fn read_components(storage: &JsonStorage) -> Result<Vec<ComponentStatusDto>,
}) })
} }
pub fn read_startup_snapshot(
storage: &JsonStorage,
) -> Result<StartupSnapshotResponse, CommandError> {
let detected_proxyfier = detect_proxyfier_install();
let detected_singbox = detect_singbox_install();
let saved_state = read_saved_state_with_proxifyre_config(
storage,
detected_proxyfier
.as_ref()
.and_then(|detected| detected.config_path.as_deref()),
)?;
let stored_components = storage.read_components().map_err(storage_error)?;
let components = resolve_component_statuses(
stored_components,
detected_proxyfier.clone(),
detected_singbox.clone(),
)
.iter()
.map(ComponentStatusDto::from)
.collect();
let proxifyre_setup_status =
build_proxifyre_setup_status_with_detection(detected_proxyfier.as_ref());
let singbox_status = read_singbox_status_with_detection(storage, detected_singbox.as_ref())?;
let singbox_setup_status = build_singbox_setup_status(detected_singbox.as_ref());
Ok(StartupSnapshotResponse {
admin_status: admin_status(),
saved_state,
components,
proxifyre_setup_status,
singbox_status,
singbox_setup_status,
})
}
pub fn read_activity(storage: &JsonStorage) -> Result<Vec<ActivityEntryDto>, CommandError> { pub fn read_activity(storage: &JsonStorage) -> Result<Vec<ActivityEntryDto>, CommandError> {
storage storage
.read_activity() .read_activity()
@@ -1315,13 +1372,20 @@ pub fn apply_profiles_with_services(
pub fn read_singbox_status( pub fn read_singbox_status(
storage: &JsonStorage, storage: &JsonStorage,
) -> Result<LocalSingBoxStatusResponse, CommandError> {
let detected = detect_singbox_install();
read_singbox_status_with_detection(storage, detected.as_ref())
}
fn read_singbox_status_with_detection(
storage: &JsonStorage,
detected: Option<&DetectedSingBox>,
) -> Result<LocalSingBoxStatusResponse, CommandError> { ) -> Result<LocalSingBoxStatusResponse, CommandError> {
let config = storage.read_local_singbox_config().map_err(storage_error)?; let config = storage.read_local_singbox_config().map_err(storage_error)?;
let cache = storage let cache = storage
.read_singbox_subscription_cache() .read_singbox_subscription_cache()
.map_err(storage_error)?; .map_err(storage_error)?;
let detected = detect_singbox_install(); let component = singbox_component_from_detection(detected);
let component = singbox_component_from_detection(detected.as_ref());
Ok(LocalSingBoxStatusResponse { Ok(LocalSingBoxStatusResponse {
config: LocalSingBoxConfigDto::from(&config), config: LocalSingBoxConfigDto::from(&config),
@@ -1968,7 +2032,7 @@ fn control_singbox_service(
config_source, config_source,
config_target.as_deref(), config_target.as_deref(),
); );
let output = Command::new("powershell") let output = command_no_window("powershell")
.args([ .args([
"-NoProfile", "-NoProfile",
"-NonInteractive", "-NonInteractive",
@@ -2824,7 +2888,7 @@ Write-ServiceResult $false 'stop_failed' $status $processId
"# "#
); );
let output = Command::new("powershell") let output = command_no_window("powershell")
.args([ .args([
"-NoProfile", "-NoProfile",
"-NonInteractive", "-NonInteractive",
@@ -3086,9 +3150,15 @@ fn uninstall_proxifyre_component() -> Result<ComponentStatusDto, CommandError> {
} }
fn build_proxifyre_setup_status() -> ProxiFyreSetupStatusDto { fn build_proxifyre_setup_status() -> ProxiFyreSetupStatusDto {
let proxifyre = detect_proxyfier_install();
build_proxifyre_setup_status_with_detection(proxifyre.as_ref())
}
fn build_proxifyre_setup_status_with_detection(
proxifyre: Option<&DetectedProxyfier>,
) -> ProxiFyreSetupStatusDto {
let vc_runtime = detect_vc_runtime(); let vc_runtime = detect_vc_runtime();
let packet_filter = detect_windows_packet_filter(); let packet_filter = detect_windows_packet_filter();
let proxifyre = detect_proxyfier_install();
let vc_runtime_item = setup_item_from_program( let vc_runtime_item = setup_item_from_program(
"vc-runtime", "vc-runtime",
@@ -3199,7 +3269,7 @@ if ($null -ne $program) {{
escape_powershell_single(pattern) escape_powershell_single(pattern)
); );
let output = Command::new("powershell") let output = command_no_window("powershell")
.args([ .args([
"-NoProfile", "-NoProfile",
"-NonInteractive", "-NonInteractive",
@@ -3379,7 +3449,7 @@ fn write_powershell_script(path: &Path, script: &str) -> std::io::Result<()> {
} }
fn run_powershell_command(script: &str) -> std::io::Result<Output> { fn run_powershell_command(script: &str) -> std::io::Result<Output> {
Command::new("powershell") command_no_window("powershell")
.args([ .args([
"-NoProfile", "-NoProfile",
"-NonInteractive", "-NonInteractive",
@@ -3392,7 +3462,7 @@ fn run_powershell_command(script: &str) -> std::io::Result<Output> {
} }
fn run_powershell_file(script_path: &Path) -> std::io::Result<Output> { fn run_powershell_file(script_path: &Path) -> std::io::Result<Output> {
Command::new("powershell") command_no_window("powershell")
.args([ .args([
"-NoProfile", "-NoProfile",
"-NonInteractive", "-NonInteractive",

View File

@@ -2,11 +2,11 @@ use crate::models::{
ComponentId, ComponentState, ComponentStatus, DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT, ComponentId, ComponentState, ComponentStatus, DEFAULT_LOCAL_SINGBOX_INSTALL_ROOT,
DEFAULT_LOCAL_SINGBOX_SERVICE_NAME, DEFAULT_LOCAL_SINGBOX_SERVICE_NAME,
}; };
use crate::process::command_no_window;
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
env, env,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::Command,
}; };
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
@@ -460,7 +460,7 @@ fn same_path(left: &Path, right: &Path) -> bool {
} }
fn powershell_bool(script: &str) -> bool { fn powershell_bool(script: &str) -> bool {
Command::new("powershell") command_no_window("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", script]) .args(["-NoProfile", "-NonInteractive", "-Command", script])
.output() .output()
.ok() .ok()
@@ -492,7 +492,7 @@ $items |
ConvertTo-Json -Compress ConvertTo-Json -Compress
"#; "#;
let Ok(output) = Command::new("powershell") let Ok(output) = command_no_window("powershell")
.args(["-NoProfile", "-NonInteractive", "-Command", script]) .args(["-NoProfile", "-NonInteractive", "-Command", script])
.output() .output()
else { else {

View File

@@ -4,6 +4,7 @@ mod activity;
mod commands; mod commands;
mod component_detection; mod component_detection;
mod models; mod models;
mod process;
mod singbox_service; mod singbox_service;
mod storage; mod storage;
mod subscription; mod subscription;
@@ -38,6 +39,7 @@ fn main() {
commands::get_status, commands::get_status,
commands::get_admin_status, commands::get_admin_status,
commands::restart_as_admin, commands::restart_as_admin,
commands::get_startup_snapshot,
commands::get_profiles, commands::get_profiles,
commands::get_saved_state, commands::get_saved_state,
commands::save_profile, commands::save_profile,

18
src-tauri/src/process.rs Normal file
View File

@@ -0,0 +1,18 @@
use std::{ffi::OsStr, process::Command};
pub fn command_no_window(program: impl AsRef<OsStr>) -> Command {
let mut command = Command::new(program);
hide_console_window(&mut command);
command
}
#[cfg(windows)]
fn hide_console_window(command: &mut Command) {
use std::os::windows::process::CommandExt;
const CREATE_NO_WINDOW: u32 = 0x08000000;
command.creation_flags(CREATE_NO_WINDOW);
}
#[cfg(not(windows))]
fn hide_console_window(_command: &mut Command) {}

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "ProxyWarden", "productName": "ProxyWarden",
"version": "0.1.0", "version": "1.0.0",
"identifier": "ru.dokops.proxywarden.windows", "identifier": "ru.dokops.proxywarden.windows",
"build": { "build": {
"beforeDevCommand": "npm run dev", "beforeDevCommand": "npm run dev",

View File

@@ -6,6 +6,8 @@ mod commands;
mod component_detection; mod component_detection;
#[path = "../src/models.rs"] #[path = "../src/models.rs"]
mod models; mod models;
#[path = "../src/process.rs"]
mod process;
#[path = "../src/adapters/proxifyre.rs"] #[path = "../src/adapters/proxifyre.rs"]
mod proxifyre; mod proxifyre;
#[path = "../src/adapters/proxy_router.rs"] #[path = "../src/adapters/proxy_router.rs"]

View File

@@ -2,6 +2,8 @@
mod component_detection; mod component_detection;
#[path = "../src/models.rs"] #[path = "../src/models.rs"]
mod models; mod models;
#[path = "../src/process.rs"]
mod process;
use component_detection::{ use component_detection::{
detect_proxyfier_install_with_host, detect_singbox_install_with_host, detect_proxyfier_install_with_host, detect_singbox_install_with_host,

View File

@@ -1,5 +1,7 @@
#[path = "../src/models.rs"] #[path = "../src/models.rs"]
mod models; mod models;
#[path = "../src/process.rs"]
mod process;
#[path = "../src/adapters/proxifyre.rs"] #[path = "../src/adapters/proxifyre.rs"]
mod proxifyre; mod proxifyre;
#[path = "../src/adapters/proxy_router.rs"] #[path = "../src/adapters/proxy_router.rs"]

View File

@@ -6,6 +6,8 @@ mod commands;
mod component_detection; mod component_detection;
#[path = "../src/models.rs"] #[path = "../src/models.rs"]
mod models; mod models;
#[path = "../src/process.rs"]
mod process;
#[path = "../src/adapters/proxifyre.rs"] #[path = "../src/adapters/proxifyre.rs"]
mod proxifyre; mod proxifyre;
#[path = "../src/adapters/proxy_router.rs"] #[path = "../src/adapters/proxy_router.rs"]

View File

@@ -2,6 +2,8 @@
mod component_detection; mod component_detection;
#[path = "../src/models.rs"] #[path = "../src/models.rs"]
mod models; mod models;
#[path = "../src/process.rs"]
mod process;
#[path = "../src/singbox_service.rs"] #[path = "../src/singbox_service.rs"]
mod singbox_service; mod singbox_service;

View File

@@ -43,6 +43,15 @@ export interface SavedStateResponse {
generatedConfigPath: string; generatedConfigPath: string;
} }
export interface StartupSnapshotResponse {
adminStatus: AdminStatusResponse;
savedState: SavedStateResponse;
components: ComponentStatus[];
proxifyreSetupStatus: ProxiFyreSetupStatus;
singboxStatus: LocalSingBoxStatusResponse;
singboxSetupStatus: SingBoxSetupStatus;
}
export interface ProxiFyreSetupItem { export interface ProxiFyreSetupItem {
id: string; id: string;
name: string; name: string;
@@ -150,6 +159,10 @@ export function restartAsAdmin(): Promise<void> {
return invoke<void>('restart_as_admin'); return invoke<void>('restart_as_admin');
} }
export function getStartupSnapshot(): Promise<StartupSnapshotResponse> {
return invoke<StartupSnapshotResponse>('get_startup_snapshot');
}
export function getSavedState(): Promise<SavedStateResponse> { export function getSavedState(): Promise<SavedStateResponse> {
return invoke<SavedStateResponse>('get_saved_state'); return invoke<SavedStateResponse>('get_saved_state');
} }

View File

@@ -6,12 +6,12 @@ import {
fetchSingBoxSubscription, fetchSingBoxSubscription,
forgetSingBoxSubscription, forgetSingBoxSubscription,
generateSingBoxConfig, generateSingBoxConfig,
getAdminStatus,
getComponents, getComponents,
getProxiFyreSetupStatus, getProxiFyreSetupStatus,
getSavedState, getSavedState,
getSingBoxSetupStatus, getSingBoxSetupStatus,
getSingBoxStatus, getSingBoxStatus,
getStartupSnapshot,
installProxiFyre, installProxiFyre,
installSingBox, installSingBox,
pingAllSingBoxServers, pingAllSingBoxServers,
@@ -268,11 +268,21 @@ export function App() {
}, [activeLogId]); }, [activeLogId]);
async function refresh() { async function refresh() {
void refreshAdminStatus();
setIsLoading(true); setIsLoading(true);
setIsDetectingComponents(true);
try { try {
const saved = await getSavedState(); const snapshot = await getStartupSnapshot();
applySavedState(saved.profiles, saved.targets, saved.generatedConfigPath); setAdminStatus(snapshot.adminStatus);
setComponents(snapshot.components);
setSetupStatus(snapshot.proxifyreSetupStatus);
setSingBoxStatus(snapshot.singboxStatus);
setSingBoxSetupStatus(snapshot.singboxSetupStatus);
applySavedState(
snapshot.savedState.profiles,
snapshot.savedState.targets,
snapshot.savedState.generatedConfigPath,
snapshot.singboxStatus,
);
} catch { } catch {
showNotice({ showNotice({
kind: 'info', kind: 'info',
@@ -281,17 +291,7 @@ export function App() {
}); });
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} setIsDetectingComponents(false);
void refreshComponents();
}
async function refreshAdminStatus() {
try {
const status = await getAdminStatus();
setAdminStatus(status);
} catch {
setAdminStatus(null);
} }
} }
@@ -310,37 +310,6 @@ export function App() {
} }
} }
async function refreshComponents() {
setIsDetectingComponents(true);
try {
const [detectedComponents, detectedSetupStatus, detectedSingBoxStatus, detectedSingBoxSetupStatus] = await Promise.all([
getComponents(),
getProxiFyreSetupStatus(),
getSingBoxStatus(),
getSingBoxSetupStatus(),
]);
setComponents(detectedComponents);
setSetupStatus(detectedSetupStatus);
setSingBoxStatus(detectedSingBoxStatus);
setSingBoxSetupStatus(detectedSingBoxSetupStatus);
setAppliedSnapshot((current) => {
if (!current || current.routeMode !== 'local-singbox' || current.selectedServerTag) return current;
return {
...current,
selectedServerTag: normalizeServerTag(detectedSingBoxStatus.config.selectedServerTag),
};
});
} catch (error) {
showNotice({
kind: 'error',
title: 'Компоненты не проверены',
text: errorMessage(error),
});
} finally {
setIsDetectingComponents(false);
}
}
function applySavedState( function applySavedState(
profiles: Profile[], profiles: Profile[],
targets: Target[], targets: Target[],