feat: integrate Bugtracker module, UpdateService enhancements & Token hierarchy
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user