feat: UpdateAgent Selbstaktualisierung & kopflose Installation, .NET 10 Migration, Backend- & SDK-Fixes

- .NET 10 Migration: Alle .NET-Projekte auf net10.0 umgestellt (#459)
- Client-SDK: ResolveAgentPath bereinigt und DLL-Pruefung ergaenzt (#458)
- Packager: Commit-Ermittlung aus publishDir / Assembly InformationalVersion (#456)
- BuildInfo.targets: ExitCode-Abfang und Regex-Validierung gegen Git-Fehlertexte (#363)
- UpdateService: Re-Publish erhaelt bestehende Manifeste und Werte (#457)
- Bugtracker API: on_roadmap-Filter in manage?action=list durchgereicht (#349)
- Watchdog API: project_slug-Filter in GET /status ergaenzt (#420)
- UpdateService API: no_release_for_platform mit verfuegbaren Plattformen (#421)
- Bugtracker Core: Fehler-Normalisierung gegen zersplitterte error_hashes (#419)
- UpdateAgent: Kopfloser Installationsmodus via --answers (#89)
- Installer & Agent: Automatische Selbstaktualisierung beim Start
- Tooling: AGENTS.md, CLAUDE.md und Standalone-CLI scripts/dc.py hinzugefuegt
This commit is contained in:
Deploymentcenter Bot
2026-09-02 23:43:37 +02:00
parent 8447b2d10a
commit 087f4392a9
26 changed files with 1582 additions and 67 deletions
@@ -55,12 +55,22 @@ namespace Deploymentcenter.UpdateAgent
targetDir = Path.GetFullPath(targetDir);
// Selbststaendige Aktualisierungspruefung des Agenten beim Start
var selfUpdateResult = await TrySelfUpdateAsync(baseUrl, platform, args);
if (selfUpdateResult.HasValue)
{
return selfUpdateResult.Value;
}
string? answersPath = GetArg(args, "--answers");
var options = new AgentOptions
{
BaseUrl = baseUrl,
Project = project,
Channel = channel,
Platform = platform,
Version = version,
TargetDir = targetDir,
RestartApp = restartApp,
RequireSignature = HasFlag(args, "--require-signature"),
@@ -68,6 +78,7 @@ namespace Deploymentcenter.UpdateAgent
AgentDirectory = agentDir,
WaitForPid = ParsePid(GetArg(args, "--wait-for-pid")),
WaitTimeoutSeconds = ParseInt(GetArg(args, "--wait-timeout"), 60),
AnswersPath = answersPath,
// Die Release-Ablage verlangt Zugangsdaten. Im laufenden
// Betrieb ist das der Lizenzschluessel der Anwendung; bei
@@ -1216,9 +1227,199 @@ namespace Deploymentcenter.UpdateAgent
Console.WriteLine(" --download-password <pw> dazugehoeriges Passwort");
Console.WriteLine(" --pubkey <datei> Oeffentlicher Schluessel zur Signaturpruefung");
Console.WriteLine(" --require-signature Ohne gueltige Signatur nicht installieren");
Console.WriteLine(" --answers <datei> Antwortdatei fuer unbeaufsichtigte Installation (JSON)");
Console.WriteLine(" --no-self-update Selbstaktualisierung beim Start unterdruecken");
Console.WriteLine();
return 0;
}
/// <summary>
/// Prueft beim Start selbststaendig, ob es im Deploymentcenter eine neuere
/// Fassung des Update-Agenten gibt, und aktualisiert die eigene Binaerdatei
/// vor der Ausfuehrung.
/// </summary>
private static async Task<int?> TrySelfUpdateAsync(string baseUrl, string platform, string[] args)
{
if (HasFlag(args, "--no-self-update") || HasFlag(args, "--skip-self-update") || HasFlag(args, "--help") || HasFlag(args, "-h"))
{
return null;
}
try
{
string? currentExe = Environment.ProcessPath;
if (string.IsNullOrWhiteSpace(currentExe) || !File.Exists(currentExe))
{
currentExe = Process.GetCurrentProcess().MainModule?.FileName;
}
if (string.IsNullOrWhiteSpace(currentExe) || !File.Exists(currentExe))
{
return null;
}
// In Entwicklungsumgebungen (Aufruf via "dotnet update-agent.dll") keinen Selbstaustausch ausfuehren
string exeName = Path.GetFileNameWithoutExtension(currentExe);
if (string.Equals(exeName, "dotnet", StringComparison.OrdinalIgnoreCase))
{
return null;
}
string ownVersion = GetOwnVersion();
string manifestUrl = $"{baseUrl.TrimEnd('/')}/installer/installer.json";
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
using var request = new HttpRequestMessage(HttpMethod.Get, manifestUrl);
using var response = await HttpClient.SendAsync(request, cts.Token);
if (!response.IsSuccessStatusCode)
{
return null;
}
string json = await response.Content.ReadAsStringAsync(cts.Token);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
if (!root.TryGetProperty("version", out var verProp) || verProp.ValueKind != JsonValueKind.String)
{
return null;
}
string remoteVersion = verProp.GetString() ?? "";
if (string.IsNullOrWhiteSpace(remoteVersion) || !UpdateClient.IsVersionNewer(ownVersion, remoteVersion))
{
return null;
}
if (!root.TryGetProperty("binaries", out var binariesProp) || binariesProp.ValueKind != JsonValueKind.Array)
{
return null;
}
string normalizedPlatform = PlatformId.Normalize(platform);
string? remoteFileName = null;
string? expectedSha256 = null;
foreach (var bin in binariesProp.EnumerateArray())
{
string p = bin.TryGetProperty("platform", out var pProp) ? pProp.GetString() ?? "" : "";
if (string.Equals(PlatformId.Normalize(p), normalizedPlatform, StringComparison.OrdinalIgnoreCase))
{
remoteFileName = bin.TryGetProperty("file", out var fProp) ? fProp.GetString() : null;
expectedSha256 = bin.TryGetProperty("sha256", out var sProp) ? sProp.GetString() : null;
break;
}
}
if (string.IsNullOrWhiteSpace(remoteFileName) || string.IsNullOrWhiteSpace(expectedSha256))
{
return null;
}
string downloadUrl = $"{baseUrl.TrimEnd('/')}/installer/{remoteFileName}";
string tempFile = Path.Combine(Path.GetTempPath(), "dc_selfupdate_" + Guid.NewGuid().ToString("N"));
try
{
using (var downloadCts = new CancellationTokenSource(TimeSpan.FromSeconds(30)))
using (var dlResp = await HttpClient.GetAsync(downloadUrl, downloadCts.Token))
{
if (!dlResp.IsSuccessStatusCode)
return null;
using (var fs = File.Create(tempFile))
{
await dlResp.Content.CopyToAsync(fs, downloadCts.Token);
}
}
string actualSha256 = ComputeSha256(tempFile);
if (!string.Equals(actualSha256, expectedSha256, StringComparison.OrdinalIgnoreCase))
{
return null;
}
// Binaerdatei austauschen
if (OperatingSystem.IsWindows())
{
string replaced = currentExe + Installer.ReplacedSuffix;
if (File.Exists(replaced))
{
try { File.Delete(replaced); } catch { }
}
File.Move(currentExe, replaced);
File.Copy(tempFile, currentExe, overwrite: true);
}
else
{
File.Copy(tempFile, currentExe, overwrite: true);
try
{
File.SetUnixFileMode(currentExe,
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute |
UnixFileMode.GroupRead | UnixFileMode.GroupExecute |
UnixFileMode.OtherRead | UnixFileMode.OtherExecute);
}
catch { }
}
AnsiConsole.MarkupLine($"[bold green]Update-Agent wurde selbststaendig auf v{remoteVersion} aktualisiert.[/]");
// Mit den urspruenglichen Argumenten neu starten
var psi = new ProcessStartInfo
{
FileName = currentExe,
UseShellExecute = false,
WorkingDirectory = Environment.CurrentDirectory
};
foreach (string arg in args)
{
psi.ArgumentList.Add(arg);
}
psi.ArgumentList.Add("--no-self-update");
using var restarted = Process.Start(psi);
if (restarted != null)
{
await restarted.WaitForExitAsync();
return restarted.ExitCode;
}
}
finally
{
if (File.Exists(tempFile))
{
try { File.Delete(tempFile); } catch { }
}
}
}
catch
{
// Bei Netzwerkproblemen, Timeout oder Berechtigungsfehlern schlicht mit bisheriger Version fortfahren
}
return null;
}
private static string GetOwnVersion()
{
var asm = typeof(Program).Assembly;
var infoVer = asm.GetCustomAttributes(typeof(System.Reflection.AssemblyInformationalVersionAttribute), false)
.OfType<System.Reflection.AssemblyInformationalVersionAttribute>()
.FirstOrDefault()?.InformationalVersion;
if (!string.IsNullOrWhiteSpace(infoVer))
{
return infoVer.Split('+')[0];
}
var ver = asm.GetName().Version;
if (ver != null && ver != new Version(0, 0, 0, 0))
{
return $"{ver.Major}.{ver.Minor}.{ver.Build}";
}
return DeploymentcenterSdk.Version;
}
}
/// <summary>Gebuendelte Aufrufparameter.</summary>
@@ -1229,6 +1430,9 @@ namespace Deploymentcenter.UpdateAgent
public string Channel { get; set; } = "prod";
public string Platform { get; set; } = PlatformId.Any;
public string TargetDir { get; set; } = string.Empty;
public string Version { get; set; } = "latest";
public string? AnswersPath { get; set; }
public bool Unattended => !string.IsNullOrWhiteSpace(AnswersPath);
public string RestartApp { get; set; } = string.Empty;
public bool RequireSignature { get; set; }
public string? PublicKeyPath { get; set; }