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:
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user