191 lines
7.2 KiB
Rust
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())
|
|
}
|
|
}
|