1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
using System.Diagnostics;
using System.Text.RegularExpressions;
namespace AdamsToolkit.Core;
/// <summary>
/// Instalação silenciosa via Windows Package Manager (winget, incluído no Win10/11).
/// Os pacotes winget apontam para os instaladores oficiais dos fabricantes —
/// a app continua sem alojar nem redistribuir ficheiros de terceiros.
/// </summary>
public static class WingetInstaller
{
private static bool? _available;
public static async Task<bool> IsAvailableAsync()
{
if (_available.HasValue) return _available.Value;
try
{
using var p = Process.Start(new ProcessStartInfo("winget", "--version")
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
});
if (p == null) return (_available = false).Value;
await p.WaitForExitAsync();
_available = p.ExitCode == 0;
}
catch { _available = false; }
return _available.Value;
}
private static readonly Regex PctRx = new(@"(\d{1,3})\s*%", RegexOptions.Compiled);
private static readonly Regex SizeRx = new(
@"([\d.,]+)\s*(KB|MB|GB)\s*/\s*([\d.,]+)\s*(KB|MB|GB)", RegexOptions.Compiled);
private static double ToMb(string num, string unit)
{
if (!double.TryParse(num.Replace(',', '.'),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out var v)) return 0;
return unit switch { "KB" => v / 1024.0, "GB" => v * 1024.0, _ => v };
}
/// <summary>Instala silenciosamente; devolve (sucesso, mensagem-final).</summary>
public static async Task<(bool ok, string message)> InstallAsync(
string wingetId, IProgress<(double pct, string label)> progress, CancellationToken ct)
{
// id vem do JSON remoto — só aceitar o formato winget (Empresa.Pacote), nunca espaços/aspas
if (!System.Text.RegularExpressions.Regex.IsMatch(wingetId ?? "", @"^[A-Za-z0-9._+-]{2,128}$"))
return (false, "ID winget inválido.");
var psi = new ProcessStartInfo("winget",
$"install -e --id {wingetId} --silent " +
"--accept-package-agreements --accept-source-agreements --disable-interactivity")
{
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true,
StandardOutputEncoding = System.Text.Encoding.UTF8,
};
try
{
using var p = Process.Start(psi);
if (p == null) return (false, "winget indisponível");
string lastLine = "";
var reader = Task.Run(async () =>
{
while (!p.StandardOutput.EndOfStream)
{
var raw = await p.StandardOutput.ReadLineAsync();
if (raw == null) break;
// winget reescreve a mesma linha com \r e caracteres de spinner/barra
foreach (var piece in raw.Split('\r'))
{
var line = new string(piece.Where(c => !char.IsControl(c) && c < 0x2500).ToArray()).Trim();
if (line.Length == 0) continue;
lastLine = line;
var size = SizeRx.Match(line);
var pct = PctRx.Match(line);
if (size.Success)
{
var done = ToMb(size.Groups[1].Value, size.Groups[2].Value);
var total = ToMb(size.Groups[3].Value, size.Groups[4].Value);
progress.Report((total > 0 ? done * 100.0 / total : -1,
$"{done:0.0} / {total:0.0} MB (winget)"));
}
else if (pct.Success && int.TryParse(pct.Groups[1].Value, out var n) && n <= 100)
{
progress.Report((n, $"{n}% (winget)"));
}
else
{
progress.Report((-1, Truncate(line, 60)));
}
}
}
}, ct);
await p.WaitForExitAsync(ct);
await reader;
if (p.ExitCode == 0) return (true, "Instalado com sucesso.");
// já instalado / sem versão aplicável — tratar como sucesso
if (lastLine.Contains("already installed", StringComparison.OrdinalIgnoreCase) ||
lastLine.Contains("já está instalado", StringComparison.OrdinalIgnoreCase) ||
(uint)p.ExitCode == 0x8A15002B)
return (true, "Já estava instalado.");
return (false, Truncate(lastLine.Length > 0 ? lastLine : $"winget saiu com código {p.ExitCode}", 80));
}
catch (OperationCanceledException) { return (false, "Cancelado."); }
catch (Exception e) { return (false, Truncate(e.Message, 80)); }
}
private static string Truncate(string s, int max) =>
s.Length <= max ? s : s[..max] + "…";
}