238 lines
8.7 KiB
C#
238 lines
8.7 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|