Add admin restart flow and loading indicators

This commit is contained in:
2026-07-08 13:34:43 +03:00
parent ae13070eba
commit 838dea0e03
11 changed files with 639 additions and 127 deletions

View File

@@ -149,6 +149,15 @@ impl CommandError {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AdminStatusResponse {
pub is_windows: bool,
pub is_elevated: bool,
pub can_restart_elevated: bool,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ValidationIssue {
@@ -542,6 +551,18 @@ pub async fn get_status(
.map_err(background_task_error)?
}
#[tauri::command]
pub fn get_admin_status() -> AdminStatusResponse {
admin_status()
}
#[tauri::command]
pub fn restart_as_admin(app: tauri::AppHandle) -> Result<(), CommandError> {
launch_app_as_admin()?;
app.exit(0);
Ok(())
}
#[tauri::command]
pub fn get_saved_state(
state: tauri::State<'_, CommandState>,
@@ -814,6 +835,87 @@ pub async fn uninstall_singbox() -> Result<ComponentStatusDto, CommandError> {
.map_err(background_task_error)?
}
pub fn admin_status() -> AdminStatusResponse {
let is_windows = cfg!(windows);
let is_elevated = is_running_elevated();
let message = if !is_windows {
"Проверка прав администратора нужна только в Windows.".to_string()
} else if is_elevated {
"ProxyWarden уже запущен от имени администратора.".to_string()
} else {
"Для установки компонентов и управления службами можно перезапустить ProxyWarden от имени администратора один раз.".to_string()
};
AdminStatusResponse {
is_windows,
is_elevated,
can_restart_elevated: is_windows && !is_elevated,
message,
}
}
fn launch_app_as_admin() -> Result<(), CommandError> {
if !cfg!(windows) {
return Err(CommandError::new(
"admin_restart_unsupported",
"Перезапуск от имени администратора доступен только в Windows.",
));
}
if is_running_elevated() {
return Ok(());
}
let exe_path = env::current_exe().map_err(|error| {
CommandError::new(
"admin_restart_failed",
format!("Не удалось определить путь текущего приложения: {error}"),
)
})?;
let working_dir = env::current_dir().ok();
let working_dir_arg = working_dir
.as_ref()
.map(|path| {
format!(
" -WorkingDirectory '{}'",
escape_powershell_single(&path.display().to_string())
)
})
.unwrap_or_default();
let script = format!(
r#"
$ErrorActionPreference = 'Stop'
try {{
Start-Process -FilePath '{}' -Verb RunAs{}
exit 0
}} catch {{
Write-Error ($_ | Out-String)
exit 1
}}
"#,
escape_powershell_single(&exe_path.display().to_string()),
working_dir_arg
);
let output = run_powershell_command(&script).map_err(|error| {
CommandError::new(
"admin_restart_failed",
format!("Не удалось запросить права администратора: {error}"),
)
})?;
if output.status.success() {
return Ok(());
}
Err(CommandError::new(
"admin_restart_failed",
powershell_output_message(
&output,
"Перезапуск от имени администратора отменен или не был запущен.",
),
))
}
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)?;
@@ -1933,16 +2035,11 @@ fn run_elevated_singbox_service_command(
"$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}'); exit $p.ExitCode",
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 output = if is_running_elevated() {
run_powershell_file(&script_path)
} else {
run_powershell_command(&launch_script)
};
let _ = fs::remove_file(&script_path);
@@ -2218,16 +2315,11 @@ try {{
escape_powershell_single(&result_path.display().to_string()),
escape_powershell_single(&runner_path.display().to_string())
);
let output = Command::new("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
launch_script.as_str(),
])
.output();
let output = if is_running_elevated() {
run_powershell_file(&runner_path)
} else {
run_powershell_command(&launch_script)
};
let _ = fs::remove_file(&installer_path);
let _ = fs::remove_file(&runner_path);
@@ -2781,16 +2873,11 @@ fn run_elevated_proxifyre_service_command(
"$p = Start-Process -FilePath 'powershell.exe' -Verb RunAs -Wait -PassThru -WindowStyle Hidden -ArgumentList @('-NoProfile','-ExecutionPolicy','Bypass','-File','{}'); exit $p.ExitCode",
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 output = if is_running_elevated() {
run_powershell_file(&script_path)
} else {
run_powershell_command(&launch_script)
};
let _ = fs::remove_file(&script_path);
@@ -3225,16 +3312,11 @@ try {{
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 output = if is_running_elevated() {
run_powershell_file(&script_path)
} else {
run_powershell_command(&launch_script)
};
let _ = fs::remove_file(&script_path);
@@ -3296,6 +3378,62 @@ fn write_powershell_script(path: &Path, script: &str) -> std::io::Result<()> {
fs::write(path, bytes)
}
fn run_powershell_command(script: &str) -> std::io::Result<Output> {
Command::new("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-Command",
script,
])
.output()
}
fn run_powershell_file(script_path: &Path) -> std::io::Result<Output> {
Command::new("powershell")
.args([
"-NoProfile",
"-NonInteractive",
"-ExecutionPolicy",
"Bypass",
"-File",
])
.arg(script_path)
.output()
}
fn is_running_elevated() -> bool {
if !cfg!(windows) {
return false;
}
let script = r#"([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)"#;
let Ok(output) = run_powershell_command(script) else {
return false;
};
output.status.success()
&& String::from_utf8_lossy(&output.stdout)
.trim()
.eq_ignore_ascii_case("true")
}
fn powershell_output_message(output: &Output, fallback: &str) -> String {
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
if !stderr.is_empty() {
return stderr;
}
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
if !stdout.is_empty() {
return stdout;
}
fallback.to_string()
}
pub(crate) fn install_proxifyre_script(generated_config_path: &Path) -> String {
let mut script = String::new();
script.push_str(&format!(

View File

@@ -36,6 +36,8 @@ fn main() {
.manage(commands::CommandState::default())
.invoke_handler(tauri::generate_handler![
commands::get_status,
commands::get_admin_status,
commands::restart_as_admin,
commands::get_profiles,
commands::get_saved_state,
commands::save_profile,