feat: integrate Bugtracker module, UpdateService enhancements & Token hierarchy
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
<Project>
|
||||
<!-- MSBuild target to generate BuildInfo.g.cs automatically prior to compilation -->
|
||||
<Target Name="GenerateDeploymentcenterBuildInfo" BeforeTargets="CoreCompile">
|
||||
<PropertyGroup>
|
||||
<BuildInfoFile>$(IntermediateOutputPath)BuildInfo.g.cs</BuildInfoFile>
|
||||
<BuildDateUtc>$([System.DateTime]::UtcNow.ToString("o"))</BuildDateUtc>
|
||||
<BuildVersion Condition="'$(Version)' != ''">$(Version)</BuildVersion>
|
||||
<BuildVersion Condition="'$(BuildVersion)' == ''">1.0.0</BuildVersion>
|
||||
<BuildChannel Condition="'$(BuildChannel)' == ''">prod</BuildChannel>
|
||||
</PropertyGroup>
|
||||
|
||||
<Exec Command="git rev-parse HEAD" ConsoleToMSBuild="true" IgnoreExitCode="true">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitLong" />
|
||||
</Exec>
|
||||
<Exec Command="git rev-parse --short HEAD" ConsoleToMSBuild="true" IgnoreExitCode="true">
|
||||
<Output TaskParameter="ConsoleOutput" PropertyName="GitCommitShort" />
|
||||
</Exec>
|
||||
|
||||
<PropertyGroup>
|
||||
<GitCommitLong Condition="'$(GitCommitLong)' == ''">UNKNOWN_COMMIT</GitCommitLong>
|
||||
<GitCommitShort Condition="'$(GitCommitShort)' == ''">UNKNOWN</GitCommitShort>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<BuildInfoLine Include="// <auto-generated />" />
|
||||
<BuildInfoLine Include="using System%3B" />
|
||||
<BuildInfoLine Include="namespace Deploymentcenter.Client.Models" />
|
||||
<BuildInfoLine Include="{" />
|
||||
<BuildInfoLine Include=" public static partial class BuildInfo" />
|
||||
<BuildInfoLine Include=" {" />
|
||||
<BuildInfoLine Include=" static BuildInfo()" />
|
||||
<BuildInfoLine Include=" {" />
|
||||
<BuildInfoLine Include=" Version = "$(BuildVersion)"%3B" />
|
||||
<BuildInfoLine Include=" GitCommit = "$(GitCommitLong)"%3B" />
|
||||
<BuildInfoLine Include=" GitCommitShort = "$(GitCommitShort)"%3B" />
|
||||
<BuildInfoLine Include=" BuildDateUtc = "$(BuildDateUtc)"%3B" />
|
||||
<BuildInfoLine Include=" Channel = "$(BuildChannel)"%3B" />
|
||||
<BuildInfoLine Include=" }" />
|
||||
<BuildInfoLine Include=" }" />
|
||||
<BuildInfoLine Include="}" />
|
||||
</ItemGroup>
|
||||
|
||||
<WriteLinesToFile File="$(BuildInfoFile)" Lines="@(BuildInfoLine)" Overwrite="true" WriteOnlyWhenDifferent="true" />
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="$(BuildInfoFile)" />
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
|
||||
<PackageReference Include="BouncyCastle.Cryptography" Version="2.4.0" />
|
||||
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using System;
|
||||
|
||||
namespace Deploymentcenter.Client.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Runtime accessibility for build metadata embedded at compile-time.
|
||||
/// </summary>
|
||||
public static class BuildInfo
|
||||
{
|
||||
public static string Version { get; set; } = "1.0.0";
|
||||
public static string GitCommit { get; set; } = "HEAD";
|
||||
public static string GitCommitShort { get; set; } = "HEAD";
|
||||
public static string BuildDateUtc { get; set; } = DateTime.UtcNow.ToString("o");
|
||||
public static string Channel { get; set; } = "prod";
|
||||
|
||||
public static string Summary => $"v{Version} ({GitCommitShort}) built on {BuildDateUtc} [{Channel}]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Deploymentcenter.Client.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Model for per-package manifest.json stored inside package.tar.gz
|
||||
/// and beside it on the LEMP release server.
|
||||
/// </summary>
|
||||
public class PackageManifest
|
||||
{
|
||||
[JsonPropertyName("projectId")]
|
||||
public string ProjectId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("channel")]
|
||||
public string Channel { get; set; } = "prod";
|
||||
|
||||
[JsonPropertyName("buildDate")]
|
||||
public string BuildDate { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("gitCommit")]
|
||||
public string GitCommit { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("gitCommitShort")]
|
||||
public string GitCommitShort { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("changelog")]
|
||||
public string Changelog { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("files")]
|
||||
public List<PackageFileEntry> Files { get; set; } = new List<PackageFileEntry>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// File entry item inside manifest.json for integrity checking and repair.
|
||||
/// </summary>
|
||||
public class PackageFileEntry
|
||||
{
|
||||
[JsonPropertyName("path")]
|
||||
public string Path { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("sha256")]
|
||||
public string Sha256 { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("sizeBytes")]
|
||||
public long SizeBytes { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Deploymentcenter.Client.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// Model for channel-level latest.json served by LEMP / Nginx or Deploymentcenter API.
|
||||
/// </summary>
|
||||
public class ReleaseManifest
|
||||
{
|
||||
[JsonPropertyName("projectId")]
|
||||
public string ProjectId { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("channel")]
|
||||
public string Channel { get; set; } = "prod";
|
||||
|
||||
[JsonPropertyName("latest")]
|
||||
public VersionInfo? Latest { get; set; }
|
||||
|
||||
[JsonPropertyName("versions")]
|
||||
public List<VersionInfo> Versions { get; set; } = new List<VersionInfo>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual release version details.
|
||||
/// </summary>
|
||||
public class VersionInfo
|
||||
{
|
||||
[JsonPropertyName("version")]
|
||||
public string Version { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("buildDate")]
|
||||
public string BuildDate { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("gitCommit")]
|
||||
public string GitCommit { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("gitCommitShort")]
|
||||
public string GitCommitShort { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("packageUrl")]
|
||||
public string PackageUrl { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("sha256")]
|
||||
public string Sha256 { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("sizeBytes")]
|
||||
public long SizeBytes { get; set; }
|
||||
|
||||
[JsonPropertyName("changelog")]
|
||||
public string Changelog { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("isCritical")]
|
||||
public bool IsCritical { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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