Eigener Installer mit Standard- und Portable-Modus, Updater fuer beide Varianten

This commit is contained in:
Davito committed 2026-10-14 10:34:48 +02:00
1 parent df88f8abaa
commit 92d02d0008
6 files changed
+508 -36

No files matched your search

+9 -1
View File
@@ -5,14 +5,22 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tauri::{PhysicalPosition, PhysicalSize};
fn exe_dir() -> PathBuf {
pub fn exe_dir() -> PathBuf {
std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(PathBuf::from))
.unwrap_or_else(|| PathBuf::from("."))
}
/// Der Installer legt bei der portablen Installation die Marker-Datei `portable.flag` neben die EXE.
pub fn is_portable() -> bool {
exe_dir().join("portable.flag").is_file()
}
pub fn data_dir() -> PathBuf {
if is_portable() {
return exe_dir().join("data");
}
std::env::var_os("APPDATA")
.map(|d| PathBuf::from(d).join("EuerChat"))
.unwrap_or_else(exe_dir)
+87 -16
View File
@@ -366,28 +366,65 @@ async fn download_to_file(
// Installation
// ---------------------------------------------------------------------------
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum InstallKind {
/// Mit dem Standard-Installer installiert (uninstall.exe neben der EXE), meist in Program Files.
Standard,
/// Portable Installation (portable.flag neben der EXE).
Portable,
/// Lose EXE ohne Installation, z. B. aus dem Build-Ordner.
Loose,
}
fn detect_install_kind(dir: &Path) -> InstallKind {
if dir.join("portable.flag").is_file() {
InstallKind::Portable
} else if dir.join("uninstall.exe").is_file() {
InstallKind::Standard
} else {
InstallKind::Loose
}
}
/// PowerShell-Skript: wartet auf das Ende dieser App, installiert still und startet EuerChat neu.
fn build_update_script(pid: u32, installer: &Path, current_exe: &Path, kind: InstallKind) -> String {
let quote = |p: &Path| p.to_string_lossy().replace('\'', "''");
let dir = current_exe.parent().map(quote).unwrap_or_default();
// Standard-Installationen liegen meist in Program Files und brauchen Administratorrechte (UAC).
let (args, elevate) = match kind {
InstallKind::Standard => (format!("/S /UPDATE /MODE=standard /D={dir}"), true),
InstallKind::Portable => (format!("/S /UPDATE /MODE=portable /D={dir}"), false),
InstallKind::Loose => ("/S /UPDATE /MODE=standard".to_string(), true),
};
let verb = if elevate { " -Verb RunAs" } else { "" };
let relaunch = match kind {
InstallKind::Loose => format!(
"$t = Join-Path $env:ProgramFiles 'EuerChat\\euer-chat.exe'; \
if (Test-Path $t) {{ Start-Process -FilePath $t }} else {{ Start-Process -FilePath '{}' }}",
quote(current_exe)
),
_ => format!("Start-Process -FilePath '{}'", quote(current_exe)),
};
// Bricht der Nutzer die UAC-Abfrage ab, wird trotzdem die bisherige Version neu gestartet.
format!(
"Wait-Process -Id {pid} -Timeout 30 -ErrorAction SilentlyContinue; \
try {{ Start-Process -FilePath '{installer}' -ArgumentList '{args}'{verb} -Wait }} catch {{}}; \
{relaunch}",
installer = quote(installer),
)
}
/// Startet den Installer erst, wenn diese App beendet ist, und danach EuerChat neu.
fn spawn_installer_after_exit(installer: &Path) -> std::io::Result<()> {
use std::os::windows::process::CommandExt;
let quote = |p: &Path| p.to_string_lossy().replace('\'', "''");
let current = std::env::current_exe()?;
let installed_here = current
let kind = current
.parent()
.is_some_and(|dir| dir.join("uninstall.exe").exists());
// Eine portable EXE wird per Installer in %LOCALAPPDATA%\EuerChat aktualisiert; dort neu starten.
let script = format!(
"Wait-Process -Id {pid} -Timeout 30 -ErrorAction SilentlyContinue; \
Start-Process -FilePath '{installer}' -ArgumentList '/S','/UPDATE' -Wait; \
$installed = Join-Path $env:LOCALAPPDATA 'EuerChat\\euer-chat.exe'; \
if (-not {installed_here} -and (Test-Path $installed)) {{ Start-Process -FilePath $installed }} \
else {{ Start-Process -FilePath '{current}' }}",
pid = std::process::id(),
installer = quote(installer),
installed_here = if installed_here { "$true" } else { "$false" },
current = quote(&current),
);
.map_or(InstallKind::Loose, detect_install_kind);
let script = build_update_script(std::process::id(), installer, &current, kind);
std::process::Command::new("powershell.exe")
.args(["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-Command", &script])
@@ -529,6 +566,40 @@ mod tests {
assert!(parse_expected_checksum(&other, "x.exe").unwrap_err().contains("x.exe"));
}
#[test]
fn install_kind_detection() {
let dir = std::env::temp_dir().join(format!("euerchat-kind-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
assert_eq!(detect_install_kind(&dir), InstallKind::Loose);
std::fs::write(dir.join("uninstall.exe"), b"x").unwrap();
assert_eq!(detect_install_kind(&dir), InstallKind::Standard);
std::fs::write(dir.join("portable.flag"), b"x").unwrap();
assert_eq!(detect_install_kind(&dir), InstallKind::Portable);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn update_script_per_install_kind() {
let installer = Path::new(r"C:\Users\O'Brien\AppData\Roaming\EuerChat\updates\EuerChat_0.2.0_x64-setup.exe");
let exe = Path::new(r"C:\Program Files\EuerChat\euer-chat.exe");
let standard = build_update_script(42, installer, exe, InstallKind::Standard);
assert!(standard.contains("Wait-Process -Id 42"));
assert!(standard.contains("/MODE=standard /D=C:\\Program Files\\EuerChat'"));
assert!(standard.contains("-Verb RunAs"));
assert!(standard.contains("O''Brien"), "Apostrophe müssen verdoppelt werden");
assert!(standard.contains("Start-Process -FilePath 'C:\\Program Files\\EuerChat\\euer-chat.exe'"));
let portable = build_update_script(1, installer, Path::new(r"E:\Tools\EuerChat\euer-chat.exe"), InstallKind::Portable);
assert!(portable.contains("/MODE=portable /D=E:\\Tools\\EuerChat'"));
assert!(!portable.contains("RunAs"), "portable Updates brauchen keine Administratorrechte");
let loose = build_update_script(1, installer, Path::new(r"D:\build\euer-chat.exe"), InstallKind::Loose);
assert!(loose.contains("/MODE=standard'"));
assert!(!loose.contains("/D="));
assert!(loose.contains("$env:ProgramFiles"));
}
#[test]
fn managed_installer_files() {
assert!(is_managed_installer_file("EuerChat_0.2.0_x64-setup.exe"));
+2 -11
View File
@@ -39,16 +39,7 @@
}
},
"bundle": {
"active": true,
"targets": ["nsis"],
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.ico"],
"shortDescription": "EuerChat - IRC-Chat und Livestream-Client fuer euer.tv",
"windows": {
"nsis": {
"installMode": "currentUser",
"languages": ["German"],
"displayLanguageSelector": false
}
}
"active": false,
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/icon.ico"]
}
}