feat: integrate Bugtracker module, UpdateService enhancements & Token hierarchy

This commit is contained in:
Deploymentcenter Bot
2026-08-06 12:45:58 +02:00
parent 70b35f7b8b
commit d71f90cdfc
28 changed files with 3494 additions and 32 deletions
@@ -0,0 +1,237 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Deploymentcenter.Client.Models;
namespace Deploymentcenter.Client
{
public class UpdateCheckResult
{
public bool UpdateAvailable { get; set; }
public bool IsCritical { get; set; }
public VersionInfo? LatestRelease { get; set; }
public ReleaseManifest? FullManifest { get; set; }
public string Message { get; set; } = string.Empty;
public Exception? Error { get; set; }
}
public class IntegrityCheckResult
{
public bool IsValid { get; set; } = true;
public List<string> MissingFiles { get; } = new List<string>();
public List<string> CorruptedFiles { get; } = new List<string>();
public int TotalCheckedFiles { get; set; }
}
public class UpdateClient
{
private static readonly HttpClient SharedHttpClient = new HttpClient();
private readonly HttpClient _httpClient;
public UpdateClient(HttpClient? httpClient = null)
{
_httpClient = httpClient ?? SharedHttpClient;
}
/// <summary>
/// Checks for update availability against LEMP static latest.json or Deploymentcenter API.
/// </summary>
public async Task<UpdateCheckResult> CheckForUpdateAsync(
string baseUrl,
string projectId,
string currentVersion,
string channel = "prod",
CancellationToken cancellationToken = default)
{
var result = new UpdateCheckResult();
try
{
string cleanBaseUrl = baseUrl.TrimEnd('/');
// Primary check: LEMP static channel latest.json
// Path pattern: https://domain/releases/{ProjectId}/{channel}/latest.json
string staticUrl = $"{cleanBaseUrl}/releases/{projectId}/{channel}/latest.json";
HttpResponseMessage response;
try
{
response = await _httpClient.GetAsync(staticUrl, cancellationToken).ConfigureAwait(false);
}
catch
{
// Fallback check: Deploymentcenter API endpoint
// Path pattern: https://domain/api/updateservice/v1/check?product={projectId}&version={currentVersion}
string apiUrl = $"{cleanBaseUrl}/api/updateservice/v1/check?product={Uri.EscapeDataString(projectId)}&version={Uri.EscapeDataString(currentVersion)}";
response = await _httpClient.GetAsync(apiUrl, cancellationToken).ConfigureAwait(false);
}
if (!response.IsSuccessStatusCode)
{
result.Message = $"HTTP Error {response.StatusCode} during update check";
return result;
}
string json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
using var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
// Handle static latest.json format
if (root.TryGetProperty("latest", out var latestProp) && latestProp.ValueKind == JsonValueKind.Object)
{
var manifest = JsonSerializer.Deserialize<ReleaseManifest>(json);
if (manifest?.Latest != null)
{
result.FullManifest = manifest;
result.LatestRelease = manifest.Latest;
if (IsVersionNewer(currentVersion, manifest.Latest.Version))
{
result.UpdateAvailable = true;
result.IsCritical = manifest.Latest.IsCritical;
result.Message = $"New release v{manifest.Latest.Version} available.";
}
else
{
result.Message = "Application is up to date.";
}
}
}
// Handle API response format
else if (root.TryGetProperty("update_available", out var availProp))
{
bool available = availProp.GetBoolean();
result.UpdateAvailable = available;
if (root.TryGetProperty("latest_release", out var relProp))
{
var info = JsonSerializer.Deserialize<VersionInfo>(relProp.GetRawText());
result.LatestRelease = info;
result.IsCritical = info?.IsCritical ?? false;
}
result.Message = available ? "Update available." : "Application is up to date.";
}
}
catch (Exception ex)
{
result.Error = ex;
result.Message = $"Update check failed: {ex.Message}";
}
return result;
}
/// <summary>
/// Validates local application integrity against manifest.json.
/// </summary>
public static IntegrityCheckResult VerifyIntegrity(string localAppDir, PackageManifest manifest)
{
var result = new IntegrityCheckResult();
if (manifest == null || manifest.Files == null || manifest.Files.Count == 0)
{
return result;
}
foreach (var entry in manifest.Files)
{
result.TotalCheckedFiles++;
string fullPath = Path.Combine(localAppDir, entry.Path.Replace('/', Path.DirectorySeparatorChar));
if (!File.Exists(fullPath))
{
result.IsValid = false;
result.MissingFiles.Add(entry.Path);
continue;
}
if (!string.IsNullOrEmpty(entry.Sha256))
{
string computedHash = ComputeSha256(fullPath);
if (!string.Equals(computedHash, entry.Sha256, StringComparison.OrdinalIgnoreCase))
{
result.IsValid = false;
result.CorruptedFiles.Add(entry.Path);
}
}
}
return result;
}
/// <summary>
/// Launches UpdateAgent process with appropriate parameters and optionally exits current application.
/// </summary>
public static bool LaunchUpdateAgent(
string agentPath,
string projectId,
string channel = "prod",
string action = "update",
string version = "latest",
string? targetDir = null,
bool exitCurrentApp = true)
{
if (!File.Exists(agentPath))
{
return false;
}
targetDir ??= AppDomain.CurrentDomain.BaseDirectory;
var args = new StringBuilder();
args.Append($"--project \"{projectId}\" ");
args.Append($"--channel \"{channel}\" ");
args.Append($"--action \"{action}\" ");
args.Append($"--version \"{version}\" ");
args.Append($"--target-dir \"{targetDir}\"");
var startInfo = new ProcessStartInfo
{
FileName = agentPath,
Arguments = args.ToString(),
UseShellExecute = true
};
Process.Start(startInfo);
if (exitCurrentApp)
{
Environment.Exit(0);
}
return true;
}
public static string ComputeSha256(string filePath)
{
using var sha256 = SHA256.Create();
using var stream = File.OpenRead(filePath);
byte[] hash = sha256.ComputeHash(stream);
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
}
public static bool IsVersionNewer(string currentVer, string remoteVer)
{
if (string.IsNullOrEmpty(remoteVer)) return false;
if (string.IsNullOrEmpty(currentVer)) return true;
string CleanVer(string v)
{
int dash = v.IndexOf('-');
return dash > 0 ? v.Substring(0, dash) : v;
}
if (Version.TryParse(CleanVer(currentVer), out var cVer) &&
Version.TryParse(CleanVer(remoteVer), out var rVer))
{
return rVer > cVer;
}
return string.Compare(remoteVer, currentVer, StringComparison.OrdinalIgnoreCase) > 0;
}
}
}