Files
VitosUpdater/src-tauri/src/managers/winget/mod.rs
T

191 lines
7.2 KiB
Rust

//! 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())
}
}