Erste Version: Tauri-Grundgerüst, WinGet-Anbindung, Selbst-Updater und Release-Skript
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
commit
e0773ba299
84 files changed
+12688
No files matched your search
@@ -0,0 +1,190 @@
|
||||
//! WinGet-Manager: PowerShell-Modul (JSON) wenn vorhanden, sonst Tabellenparser der Kommandozeile.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::PackageManager;
|
||||
use crate::error::{AppError, AppResult};
|
||||
use crate::models::{ExtraInfo, ManagerId, ManagerInfo, Package, PackageDetails, Source};
|
||||
use crate::process::{self, CmdOutput};
|
||||
|
||||
pub mod parser;
|
||||
pub mod ps;
|
||||
|
||||
/// Exit-Code von WinGet bei „keine Treffer“ (0x8A150014), per Test mit WinGet 1.29 ermittelt.
|
||||
const EXIT_NO_MATCH: i32 = 0x8A15_0014_u32 as i32;
|
||||
/// Gemeinsame Argumente, damit WinGet nie auf Eingaben wartet.
|
||||
const COMMON_ARGS: [&str; 2] = ["--accept-source-agreements", "--disable-interactivity"];
|
||||
const QUERY_TIMEOUT: Duration = Duration::from_secs(180);
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
struct Detected {
|
||||
path: Option<PathBuf>,
|
||||
module: Option<ps::ModuleInfo>,
|
||||
}
|
||||
|
||||
/// WinGet-Implementierung von [`PackageManager`].
|
||||
#[derive(Default)]
|
||||
pub struct WingetManager {
|
||||
state: RwLock<Detected>,
|
||||
}
|
||||
|
||||
/// Sucht `winget.exe` im PATH und danach in `%LOCALAPPDATA%\Microsoft\WindowsApps`.
|
||||
pub fn locate_winget() -> Option<PathBuf> {
|
||||
if let Ok(path) = which::which("winget") {
|
||||
return Some(path);
|
||||
}
|
||||
let local = std::env::var_os("LOCALAPPDATA")?;
|
||||
let candidate = Path::new(&local).join("Microsoft").join("WindowsApps").join("winget.exe");
|
||||
candidate.is_file().then_some(candidate)
|
||||
}
|
||||
|
||||
/// Formatiert einen WinGet-Exit-Code als hexadezimale Zahl, z. B. `0x8A150014`.
|
||||
pub fn format_exit_code(code: i32) -> String {
|
||||
format!("0x{:08X}", code as u32)
|
||||
}
|
||||
|
||||
impl WingetManager {
|
||||
/// Erstellt einen Manager ohne Erkennung. [`PackageManager::detect`] füllt den Zustand.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
async fn exe(&self) -> AppResult<PathBuf> {
|
||||
if let Some(path) = self.state.read().await.path.clone() {
|
||||
return Ok(path);
|
||||
}
|
||||
self.detect().await;
|
||||
self.state
|
||||
.read()
|
||||
.await
|
||||
.path
|
||||
.clone()
|
||||
.ok_or_else(|| AppError::ManagerMissing("WinGet".to_owned()))
|
||||
}
|
||||
|
||||
async fn module_shell(&self) -> Option<PathBuf> {
|
||||
self.state.read().await.module.as_ref().map(|m| m.shell.clone())
|
||||
}
|
||||
|
||||
/// Führt winget aus; „keine Treffer“ ergibt eine leere Ausgabe, andere Fehler einen Fehler.
|
||||
async fn table(&self, args: &[&str]) -> AppResult<String> {
|
||||
let exe = self.exe().await?;
|
||||
let out: CmdOutput = process::run(&exe, args, QUERY_TIMEOUT).await?;
|
||||
match out.code {
|
||||
Some(0) => Ok(out.stdout),
|
||||
Some(EXIT_NO_MATCH) => Ok(String::new()),
|
||||
// Manche Versionen liefern trotz Teilfehler (z. B. eine nicht erreichbare Quelle) eine Tabelle.
|
||||
Some(code) if !parser::parse_table(&out.stdout).is_empty() => {
|
||||
tracing::warn!(code = %format_exit_code(code), "WinGet meldete einen Fehler, Tabelle wird trotzdem verwendet");
|
||||
Ok(out.stdout)
|
||||
}
|
||||
Some(code) => Err(AppError::Command(format!(
|
||||
"WinGet wurde mit dem Fehlercode {} beendet.",
|
||||
format_exit_code(code)
|
||||
))),
|
||||
None => Err(AppError::Command("WinGet wurde unerwartet beendet.".to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl PackageManager for WingetManager {
|
||||
fn id(&self) -> ManagerId {
|
||||
ManagerId::Winget
|
||||
}
|
||||
|
||||
async fn detect(&self) -> ManagerInfo {
|
||||
let path = locate_winget();
|
||||
let version = match &path {
|
||||
Some(p) => process::run(p, &["--version"], Duration::from_secs(20))
|
||||
.await
|
||||
.ok()
|
||||
.filter(|o| o.code == Some(0))
|
||||
.map(|o| o.stdout.trim().trim_start_matches('v').to_owned())
|
||||
.filter(|v| !v.is_empty()),
|
||||
None => None,
|
||||
};
|
||||
let module = ps::detect_module().await;
|
||||
let info = ManagerInfo {
|
||||
id: ManagerId::Winget,
|
||||
name: "WinGet".to_owned(),
|
||||
available: path.is_some() && version.is_some(),
|
||||
path: path.as_ref().map(|p| p.display().to_string()),
|
||||
version: version.clone(),
|
||||
extras: vec![ExtraInfo {
|
||||
name: "Microsoft.WinGet.Client".to_owned(),
|
||||
available: module.is_some(),
|
||||
version: module.as_ref().map(|m| m.version.clone()),
|
||||
}],
|
||||
};
|
||||
*self.state.write().await = Detected { path: path.filter(|_| info.available), module };
|
||||
tracing::info!(available = info.available, version = ?info.version, module = info.extras[0].available, "WinGet erkannt");
|
||||
info
|
||||
}
|
||||
|
||||
async fn search(&self, query: &str) -> AppResult<Vec<Package>> {
|
||||
if let Some(shell) = self.module_shell().await {
|
||||
match ps::search(&shell, query).await {
|
||||
Ok(list) => return Ok(list),
|
||||
Err(err) => tracing::warn!(%err, "Modulsuche fehlgeschlagen, Kommandozeile wird verwendet"),
|
||||
}
|
||||
}
|
||||
let text = self
|
||||
.table(&["search", "--query", query, "--count", "200", COMMON_ARGS[0], COMMON_ARGS[1]])
|
||||
.await?;
|
||||
Ok(parser::parse_packages(&text))
|
||||
}
|
||||
|
||||
async fn list_installed(&self) -> AppResult<Vec<Package>> {
|
||||
if let Some(shell) = self.module_shell().await {
|
||||
match ps::list_installed(&shell).await {
|
||||
Ok(list) => return Ok(list),
|
||||
Err(err) => tracing::warn!(%err, "Modulabfrage fehlgeschlagen, Kommandozeile wird verwendet"),
|
||||
}
|
||||
}
|
||||
let text = self.table(&["list", COMMON_ARGS[0], COMMON_ARGS[1]]).await?;
|
||||
Ok(parser::parse_packages(&text))
|
||||
}
|
||||
|
||||
async fn list_updates(&self) -> AppResult<Vec<Package>> {
|
||||
if let Some(shell) = self.module_shell().await {
|
||||
match ps::list_updates(&shell).await {
|
||||
Ok(list) => return Ok(list),
|
||||
Err(err) => tracing::warn!(%err, "Modulabfrage fehlgeschlagen, Kommandozeile wird verwendet"),
|
||||
}
|
||||
}
|
||||
let text = self.table(&["upgrade", COMMON_ARGS[0], COMMON_ARGS[1]]).await?;
|
||||
Ok(parser::parse_packages(&text))
|
||||
}
|
||||
|
||||
async fn get_details(&self, package: &Package) -> AppResult<PackageDetails> {
|
||||
let mut base: Vec<&str> = vec!["show", "--id", &package.id, "--exact"];
|
||||
if let Some(source) = package.source.as_deref() {
|
||||
base.extend(["--source", source]);
|
||||
}
|
||||
base.extend(COMMON_ARGS);
|
||||
let mut with_versions = base.clone();
|
||||
with_versions.push("--versions");
|
||||
|
||||
let (show, versions) = tokio::join!(self.table(&base), self.table(&with_versions));
|
||||
let mut details = parser::parse_details(&show?);
|
||||
details.versions = parser::parse_versions(&versions.unwrap_or_default());
|
||||
Ok(details)
|
||||
}
|
||||
|
||||
async fn list_sources(&self) -> AppResult<Vec<Source>> {
|
||||
let text = self.table(&["source", "list"]).await?;
|
||||
Ok(parser::parse_table(&text)
|
||||
.into_iter()
|
||||
.map(|cells| Source {
|
||||
name: cells.first().cloned().unwrap_or_default(),
|
||||
url: cells.get(1).cloned().unwrap_or_default(),
|
||||
explicit: cells.get(2).is_some_and(|v| v.eq_ignore_ascii_case("true")),
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user