- Große Kopien laufen im Hintergrund mit Fortschritt und Abbrechen; abgebrochene Kopien werden aufgeräumt, das Original bleibt unverändert - Sichern/Wiederherstellen mit Fortschritt, keine halben ZIP-Dateien - Installer auf .NET Framework 4.8: erkennt fehlende .NET 9 Desktop Runtime und installiert sie auf Wunsch (Microsoft-Signatur wird geprüft) - Update-Hinweis über Gitea-Releases, Update mit automatischem Neustart - Fenstergröße und -position werden gemerkt - Desktop-Boxen: umbenennen, löschen, verschieben, herausziehen - Fenster und Boxen kehren auf sichtbare Bildschirme zurück - README ergänzt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
170 lines
6.8 KiB
C#
170 lines
6.8 KiB
C#
using System.ComponentModel;
|
||
using System.Diagnostics;
|
||
using System.IO;
|
||
using System.Net;
|
||
using System.Net.Http;
|
||
using System.Runtime.InteropServices;
|
||
using System.Security.Cryptography.X509Certificates;
|
||
using Microsoft.Win32;
|
||
|
||
namespace DeskCoreSetup;
|
||
|
||
/// <summary>Prüft und installiert die von DeskCore benötigte "Microsoft .NET 9 Desktop Runtime".</summary>
|
||
public static class Runtime
|
||
{
|
||
public const string DisplayName = "Microsoft .NET 9 Desktop Runtime";
|
||
public const int Major = 9;
|
||
public const string DownloadPage = "https://dotnet.microsoft.com/de-de/download/dotnet/9.0";
|
||
// offizieller Microsoft-Kurzlink auf die jeweils aktuelle 9.0-Version
|
||
private const string InstallerUrl = "https://aka.ms/dotnet/9.0/windowsdesktop-runtime-win-x64.exe";
|
||
|
||
public static bool Is64BitWindows => Environment.Is64BitOperatingSystem;
|
||
|
||
public static bool IsInstalled
|
||
{
|
||
get
|
||
{
|
||
try
|
||
{
|
||
var dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
|
||
"dotnet", "shared", "Microsoft.WindowsDesktop.App");
|
||
if (Directory.Exists(dir) && Directory.GetDirectories(dir).Any(d => Path.GetFileName(d).StartsWith($"{Major}.")))
|
||
return true;
|
||
}
|
||
catch { }
|
||
try
|
||
{
|
||
using var hklm = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry32);
|
||
using var k = hklm.OpenSubKey(@"SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedfx\Microsoft.WindowsDesktop.App");
|
||
if (k?.GetValueNames().Any(n => n.StartsWith($"{Major}.")) == true) return true;
|
||
}
|
||
catch { }
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>Lädt die Laufzeit von Microsoft und installiert sie (Windows fragt einmal nach Administratorrechten).</summary>
|
||
public static void Install(Action<double, string> progress)
|
||
{
|
||
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
|
||
var file = Path.Combine(Path.GetTempPath(), $"windowsdesktop-runtime-{Major}-win-x64.exe");
|
||
|
||
using (var http = new HttpClient { Timeout = TimeSpan.FromMinutes(20) })
|
||
{
|
||
http.DefaultRequestHeaders.UserAgent.ParseAdd("DeskCore-Setup");
|
||
using var resp = http.GetAsync(InstallerUrl, HttpCompletionOption.ResponseHeadersRead).GetAwaiter().GetResult();
|
||
resp.EnsureSuccessStatusCode();
|
||
long total = resp.Content.Headers.ContentLength ?? 0;
|
||
using var input = resp.Content.ReadAsStreamAsync().GetAwaiter().GetResult();
|
||
using var output = File.Create(file);
|
||
var buffer = new byte[81920];
|
||
long done = 0;
|
||
int read;
|
||
while ((read = input.Read(buffer, 0, buffer.Length)) > 0)
|
||
{
|
||
output.Write(buffer, 0, read);
|
||
done += read;
|
||
progress(total > 0 ? (double)done / total : 0,
|
||
total > 0 ? $"{DisplayName} wird heruntergeladen … {done / 1048576} von {total / 1048576} MB"
|
||
: $"{DisplayName} wird heruntergeladen … {done / 1048576} MB");
|
||
}
|
||
}
|
||
|
||
// Sicherheit: nur ausführen, wenn die Datei gültig von Microsoft signiert ist
|
||
if (!IsSignedByMicrosoft(file))
|
||
{
|
||
try { File.Delete(file); } catch { }
|
||
throw new InvalidOperationException("Die heruntergeladene Datei ist nicht gültig von Microsoft signiert und wurde nicht ausgeführt.");
|
||
}
|
||
|
||
progress(1, $"{DisplayName} wird installiert – bitte die Windows-Abfrage bestätigen …");
|
||
Process p;
|
||
try
|
||
{
|
||
p = Process.Start(new ProcessStartInfo(file, "/install /quiet /norestart") { UseShellExecute = true, Verb = "runas" })!;
|
||
}
|
||
catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
|
||
{
|
||
throw new InvalidOperationException($"Die Installation von {DisplayName} wurde abgebrochen.");
|
||
}
|
||
p.WaitForExit();
|
||
try { File.Delete(file); } catch { }
|
||
// 0 = ok, 3010/1641 = ok, Neustart empfohlen
|
||
if (p.ExitCode is not (0 or 3010 or 1641) || !IsInstalled)
|
||
throw new InvalidOperationException($"{DisplayName} konnte nicht installiert werden (Code {p.ExitCode}).");
|
||
}
|
||
|
||
// ---------- Signaturprüfung (WinVerifyTrust) ----------
|
||
|
||
private static bool IsSignedByMicrosoft(string file)
|
||
{
|
||
try
|
||
{
|
||
var subject = X509Certificate.CreateFromSignedFile(file).Subject;
|
||
if (subject.IndexOf("O=Microsoft Corporation", StringComparison.OrdinalIgnoreCase) < 0) return false;
|
||
}
|
||
catch { return false; }
|
||
return VerifyTrust(file);
|
||
}
|
||
|
||
private static bool VerifyTrust(string file)
|
||
{
|
||
var fileInfo = new WINTRUST_FILE_INFO
|
||
{
|
||
cbStruct = (uint)Marshal.SizeOf<WINTRUST_FILE_INFO>(),
|
||
pcwszFilePath = file,
|
||
};
|
||
var pFile = Marshal.AllocHGlobal(Marshal.SizeOf<WINTRUST_FILE_INFO>());
|
||
try
|
||
{
|
||
Marshal.StructureToPtr(fileInfo, pFile, false);
|
||
var data = new WINTRUST_DATA
|
||
{
|
||
cbStruct = (uint)Marshal.SizeOf<WINTRUST_DATA>(),
|
||
dwUIChoice = 2, // WTD_UI_NONE
|
||
fdwRevocationChecks = 0, // WTD_REVOKE_NONE
|
||
dwUnionChoice = 1, // WTD_CHOICE_FILE
|
||
pFile = pFile,
|
||
dwStateAction = 0,
|
||
dwProvFlags = 0x40, // WTD_SAFER_FLAG
|
||
};
|
||
var action = new Guid("00AAC56B-CD44-11d0-8CC2-00C04FC295EE"); // WINTRUST_ACTION_GENERIC_VERIFY_V2
|
||
return WinVerifyTrust(IntPtr.Zero, ref action, ref data) == 0;
|
||
}
|
||
finally
|
||
{
|
||
Marshal.FreeHGlobal(pFile);
|
||
}
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||
private struct WINTRUST_FILE_INFO
|
||
{
|
||
public uint cbStruct;
|
||
public string pcwszFilePath;
|
||
public IntPtr hFile;
|
||
public IntPtr pgKnownSubject;
|
||
}
|
||
|
||
[StructLayout(LayoutKind.Sequential)]
|
||
private struct WINTRUST_DATA
|
||
{
|
||
public uint cbStruct;
|
||
public IntPtr pPolicyCallbackData;
|
||
public IntPtr pSIPClientData;
|
||
public uint dwUIChoice;
|
||
public uint fdwRevocationChecks;
|
||
public uint dwUnionChoice;
|
||
public IntPtr pFile;
|
||
public uint dwStateAction;
|
||
public IntPtr hWVTStateData;
|
||
public IntPtr pwszURLReference;
|
||
public uint dwProvFlags;
|
||
public uint dwUIContext;
|
||
public IntPtr pSignatureSettings;
|
||
}
|
||
|
||
[DllImport("wintrust.dll", ExactSpelling = true, SetLastError = false, CharSet = CharSet.Unicode)]
|
||
private static extern int WinVerifyTrust(IntPtr hwnd, ref Guid pgActionID, ref WINTRUST_DATA pWVTData);
|
||
}
|