6 Commits

Author SHA1 Message Date
FrigaT
1420c2c0eb FIX сверки версий
All checks were successful
CI / build-test (push) Successful in 55s
Release / pack-and-publish (release) Successful in 55s
2025-12-12 09:48:02 +03:00
FrigaT
dea6be8094 fix
All checks were successful
CI / build-test (push) Successful in 33s
Release / pack-and-publish (release) Successful in 43s
2025-12-12 09:27:51 +03:00
FrigaT
72b0d60f3a доработано обновление
Some checks failed
CI / build-test (push) Failing after 55s
Release / pack-and-publish (release) Failing after 33s
2025-12-12 09:23:48 +03:00
9365aa16cd Добавлено временное копирование updater.exe
All checks were successful
CI / build-test (push) Successful in 37s
Release / pack-and-publish (release) Successful in 39s
2025-12-08 17:01:40 +03:00
36331e8664 Исправлена подпись
All checks were successful
CI / build-test (push) Successful in 35s
Release / pack-and-publish (release) Successful in 29s
2025-12-07 08:47:06 +03:00
d0e57d8d7b Добавлено обновление
All checks were successful
CI / build-test (push) Successful in 31s
2025-11-27 11:19:34 +03:00
12 changed files with 171 additions and 115 deletions

View File

@@ -2,9 +2,20 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GeneratePackageOnBuild>false</GeneratePackageOnBuild> <ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageId>ReleaseUpdater.Common</PackageId>
<Version>1.0.0</Version>
<Authors>FrigaT</Authors>
<Company>FrigaT</Company>
<Product>ReleaseUpdater</Product>
<Description>Система обновления приложений через github/gitea системы контроля версий.</Description>
<Copyright>Copyright © 2025 FrigaT</Copyright>
<RepositoryUrl>https://git.frigat.duckdns.org/FrigaT/ReleaseUpdater</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageProjectUrl>https://git.frigat.duckdns.org/FrigaT/ReleaseUpdater</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -17,7 +17,7 @@ public sealed class ReleaseProvider
/// <param name="token">Токен авторизации (если требуется).</param> /// <param name="token">Токен авторизации (если требуется).</param>
/// <returns>Список релизов.</returns> /// <returns>Список релизов.</returns>
public async Task<IReadOnlyList<ReleaseInfo>> GetReleasesAsync(string apiUrl, string? token = null) public async Task<IReadOnlyList<ReleaseInfo>> GetReleasesAsync(Uri apiUrl, string? token = null)
{ {
using var client = CreateClient(token); using var client = CreateClient(token);
using var resp = await client.GetAsync(apiUrl); using var resp = await client.GetAsync(apiUrl);
@@ -38,7 +38,7 @@ public sealed class ReleaseProvider
/// <param name="token">Токен авторизации (если требуется).</param> /// <param name="token">Токен авторизации (если требуется).</param>
/// <returns>Информация о релизе или null.</returns> /// <returns>Информация о релизе или null.</returns>
public async Task<ReleaseInfo?> FindReleaseAsync(string apiUrl, string? versionOrLatest, string? token = null) public async Task<ReleaseInfo?> FindReleaseAsync(Uri apiUrl, string? versionOrLatest, string? token = null)
{ {
var all = await GetReleasesAsync(apiUrl, token); var all = await GetReleasesAsync(apiUrl, token);
if (string.IsNullOrWhiteSpace(versionOrLatest) || versionOrLatest.Equals("latest", StringComparison.OrdinalIgnoreCase)) if (string.IsNullOrWhiteSpace(versionOrLatest) || versionOrLatest.Equals("latest", StringComparison.OrdinalIgnoreCase))

View File

@@ -2,8 +2,20 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<PackageId>ReleaseUpdater</PackageId>
<Version>1.0.0</Version>
<Authors>FrigaT</Authors>
<Company>FrigaT</Company>
<Product>ReleaseUpdater</Product>
<Description>Система обновления приложений через github/gitea системы контроля версий.</Description>
<Copyright>Copyright © 2025 FrigaT</Copyright>
<RepositoryUrl>https://git.frigat.duckdns.org/FrigaT/ReleaseUpdater</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageProjectUrl>https://git.frigat.duckdns.org/FrigaT/ReleaseUpdater</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
@@ -11,17 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\ReleaseUpdater.Common\ReleaseUpdater.Common.csproj" <ProjectReference Include="..\ReleaseUpdater.Common\ReleaseUpdater.Common.csproj" />
PrivateAssets="none"
IncludeAssets="all"
/>
</ItemGroup> </ItemGroup>
<Target Name="CopyProjectReferencesToPackage" AfterTargets="Pack">
<ItemGroup>
<BuildOutputInPackage Include="$(OutputPath)ReleaseUpdater.Common.dll"
TargetPath="lib\net8.0\ReleaseUpdater.Common.dll" />
</ItemGroup>
</Target>
</Project> </Project>

View File

@@ -1,5 +1,6 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO.Compression; using System.IO.Compression;
using System.Reflection;
namespace ReleaseUpdater; namespace ReleaseUpdater;
@@ -14,65 +15,47 @@ public static class ReleaseUpdaterFacade
/// </summary> /// </summary>
public static event Action? BeforeInstall; public static event Action? BeforeInstall;
/// <summary>
/// Событие вызывается после успешной установки новой версии.
/// </summary>
public static event Action? AfterInstall;
/// <summary> /// <summary>
/// Событие вызывается при ошибке обновления. /// Событие вызывается при ошибке обновления.
/// </summary> /// </summary>
public static event Action<Exception>? UpdateFailed; public static event Action<Exception>? UpdateFailed;
/// <summary>
/// Событие вызывается, если текущая версия совпадает с требуемой.
/// </summary>
public static event Action<string>? AlreadyUp;
/// <summary> /// <summary>
/// Получает список доступных версий из Gitea. /// Получает список доступных версий из Gitea.
/// </summary> /// </summary>
public static async Task<IReadOnlyList<string>> GetVersionsAsync(string apiUrl, string? token = null) public static async Task<IReadOnlyList<string>> GetVersionsAsync(Uri apiUrl, string? token = null)
{ {
var provider = new ReleaseProvider(); var provider = new ReleaseProvider();
var releases = await provider.GetReleasesAsync(apiUrl, token); var releases = await provider.GetReleasesAsync(apiUrl, token);
return releases.Select(r => r.TagName).ToList(); return releases.Select(r => r.TagName).ToList();
} }
/// <summary>
/// Обновление без Updater.exe: скачивание, распаковка и перезапуск прямо из DLL.
/// </summary>
public static async Task UpdateInlineAsync(
string apiUrl, string? token, string installPath, string appExe, string versionOrLatest = "latest")
{
try
{
var provider = new ReleaseProvider();
var release = await provider.FindReleaseAsync(apiUrl, versionOrLatest, token)
?? throw new Exception("Release not found");
var asset = release.Assets.FirstOrDefault(a => a.Name.EndsWith(".zip"))
?? throw new Exception("No zip asset found");
var downloader = new HttpAssetDownloader();
var zipPath = await downloader.DownloadAssetAsync(asset.DownloadUrl, token);
BeforeInstall?.Invoke();
ZipFile.ExtractToDirectory(zipPath, installPath, true);
AfterInstall?.Invoke();
Process.Start(Path.Combine(installPath, appExe));
Environment.Exit(0);
}
catch (Exception ex)
{
UpdateFailed?.Invoke(ex);
RestartCurrent(installPath, appExe);
}
}
/// <summary> /// <summary>
/// Обновление через внешний Updater.exe. /// Обновление через внешний Updater.exe.
/// </summary> /// </summary>
public static async Task UpdateWithExternalAsync( /// <param name="apiUrl">API github/gitea release</param>
string apiUrl, string? token, string installPath, string appExe, string versionOrLatest = "latest", string? updaterExePath = null, bool exitCurrentApp = false) /// <param name="token">Token авторизации</param>
/// <param name="installPath">Путь для установки приложения</param>
/// <param name="appExeName">Наименование файла приложения</param>
/// <param name="tempDirectory">Папка для временного хранилища zip архива</param>
/// <param name="updaterExePath">Путь к updater.exe</param>
/// <param name="versionOrLatest">Тэг с версией / "latest"</param>
/// <param name="assetMask">Маска наименовая ассета обновления. В маске может содержаться {version}</param>
public static async Task UpdateAsync(
Uri apiUrl,
string? token,
string installPath,
string appExeName,
string tempDirectory,
string updaterExePath,
string versionOrLatest = "latest",
string? assetMask = null
)
{ {
try try
{ {
@@ -80,16 +63,44 @@ public static class ReleaseUpdaterFacade
var release = await provider.FindReleaseAsync(apiUrl, versionOrLatest, token) var release = await provider.FindReleaseAsync(apiUrl, versionOrLatest, token)
?? throw new Exception("Release not found"); ?? throw new Exception("Release not found");
var asset = release.Assets.FirstOrDefault(a => a.Name.EndsWith(".zip"))
?? throw new Exception("No zip asset found"); var currentVersion = GetCurrentVersion(); // реализуй сам
if (SemVerService.Compare(release.TagName, currentVersion) == 0)
{
AlreadyUp?.Invoke(currentVersion);
return;
}
// Маска: myapp-{version}.zip
string? mask = assetMask?.Replace("{version}", release.TagName);
var asset = release.Assets.FirstOrDefault(a =>
mask != null
? a.Name.Equals(mask, StringComparison.OrdinalIgnoreCase)
: a.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)
) ?? throw new Exception("No matching asset found");
string tempNumber = $"{Guid.NewGuid():N}";
var tempUpdaterName = $"updater_{tempNumber}.exe";
if (string.IsNullOrWhiteSpace(tempDirectory))
{
tempDirectory = Path.GetTempPath();
}
else if (!Directory.Exists(tempDirectory))
{
Directory.CreateDirectory(tempDirectory);
}
var tempUpdaterPath = Path.Combine(tempDirectory, tempUpdaterName);
var downloader = new HttpAssetDownloader(); var downloader = new HttpAssetDownloader();
var zipPath = await downloader.DownloadAssetAsync(asset.DownloadUrl, token); var zipPath = await downloader.DownloadAssetAsync(asset.DownloadUrl, token, Path.Combine(tempDirectory, $"updater_{tempNumber}.zip"));
BeforeInstall?.Invoke();
if (updaterExePath == null) updaterExePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Updater.exe"); File.Copy(updaterExePath, tempUpdaterPath);
if (installPath.EndsWith("\\")) if (installPath.EndsWith("\\"))
{ {
@@ -100,55 +111,42 @@ public static class ReleaseUpdaterFacade
{ {
ZipPath = zipPath, ZipPath = zipPath,
InstallPath = installPath, InstallPath = installPath,
AppExe = appExe, AppExe = appExeName,
}; };
if (exitCurrentApp)
{
int pid = Process.GetCurrentProcess().Id; int pid = Process.GetCurrentProcess().Id;
updaterOptions.WaitProcess = pid; updaterOptions.WaitProcess = pid;
}
var args = ArgumentsToolkit.ArgumentsParser.ToArguments(updaterOptions, true); var args = ArgumentsToolkit.ArgumentsParser.ToArguments(updaterOptions, true);
BeforeInstall?.Invoke();
var process = Process.Start(new ProcessStartInfo var process = Process.Start(new ProcessStartInfo
{ {
FileName = updaterExePath, FileName = tempUpdaterPath,
Arguments = args, Arguments = args,
UseShellExecute = true, UseShellExecute = true,
WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory
}); });
if (exitCurrentApp) { Environment.Exit(0); }
process?.WaitForExit();
if (process?.ExitCode == 0)
AfterInstall?.Invoke();
else
throw new Exception($"Updater.exe завершился с кодом {process?.ExitCode}");
Environment.Exit(0); Environment.Exit(0);
} }
catch (Exception ex) catch (Exception ex)
{ {
UpdateFailed?.Invoke(ex); UpdateFailed?.Invoke(ex);
RestartCurrent(installPath, appExe);
} }
} }
private static void RestartCurrent(string installPath, string appExe) /// <summary>
/// Получение текущей версии приложения
/// </summary>
/// <returns></returns>
public static string GetCurrentVersion()
{ {
var currentApp = Path.Combine(installPath, appExe); var entryAssembly = Assembly.GetEntryAssembly();
if (File.Exists(currentApp)) var attr = entryAssembly?.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
{ return attr?.InformationalVersion.Split("+")[0]
Process.Start(new ProcessStartInfo ?? entryAssembly?.GetName().Version?.ToString()
{ ?? "unknown";
FileName = currentApp,
UseShellExecute = true,
WorkingDirectory = installPath
});
} }
} }
}

View File

@@ -11,7 +11,7 @@ public sealed class SemVerService
/// <param name="version">Строка версии (например, "3.5.2").</param> /// <param name="version">Строка версии (например, "3.5.2").</param>
/// <param name="parsed">Результат парсинга.</param> /// <param name="parsed">Результат парсинга.</param>
/// <returns>true, если парсинг успешен.</returns> /// <returns>true, если парсинг успешен.</returns>
public bool TryParse(string version, out Version parsed) public static bool TryParse(string version, out Version parsed)
{ {
var v = version.Trim().TrimStart('v'); var v = version.Trim().TrimStart('v');
return Version.TryParse(Normalize(v), out parsed); return Version.TryParse(Normalize(v), out parsed);
@@ -34,7 +34,7 @@ public sealed class SemVerService
/// <param name="v1">Первая версия.</param> /// <param name="v1">Первая версия.</param>
/// <param name="v2">Вторая версия.</param> /// <param name="v2">Вторая версия.</param>
/// <returns>-1 если v1 < v2, 0 если равны, 1 если v1 > v2.</returns> /// <returns>-1 если v1 < v2, 0 если равны, 1 если v1 > v2.</returns>
public int Compare(string v1, string v2) public static int Compare(string v1, string v2)
{ {
TryParse(v1, out var a); TryParse(v1, out var a);
TryParse(v2, out var b); TryParse(v2, out var b);

View File

@@ -14,7 +14,27 @@ internal class Program
var url = "https://git.frigat.duckdns.org/api/v1/repos/automacon/RetailUpdatesBot/releases"; var url = "https://git.frigat.duckdns.org/api/v1/repos/automacon/RetailUpdatesBot/releases";
var APIKey = "0552a77699d7506711946fc71cc6635515726bd1"; //токен var APIKey = "0552a77699d7506711946fc71cc6635515726bd1"; //токен
await ReleaseUpdaterFacade.UpdateWithExternalAsync(url, APIKey, installPath, appExe, "latest", updaterPath, true); SemVerService.TryParse("v0.1.2", out var v1);
Console.WriteLine($"v0.1.2 - {v1}");
SemVerService.TryParse(ReleaseUpdaterFacade.GetCurrentVersion(), out var v2);
Console.WriteLine($"{ReleaseUpdaterFacade.GetCurrentVersion()} - {v2}");
Console.WriteLine(SemVerService.Compare("v0.1.2", ReleaseUpdaterFacade.GetCurrentVersion()));
Console.WriteLine(SemVerService.Compare("v0.1.2", "1.0.0"));
Console.WriteLine(SemVerService.Compare("v0.1.2", "0.1.2"));
Console.WriteLine(SemVerService.Compare("v0.1.2", "0.1.1"));
Console.WriteLine(SemVerService.Compare("v0.1.2", "0.1.3"));
Console.WriteLine(SemVerService.Compare("v0.1.2", "0.2.0"));
await ReleaseUpdaterFacade.UpdateAsync(
apiUrl: new Uri(url),
token: APIKey,
installPath: installPath,
appExeName: appExe,
tempDirectory: Path.Combine(updaterPath, "Tools", "Temp"),
updaterExePath: updaterPath,
versionOrLatest: "latest",
assetMask: "RetailUpdatesBot-{version}.zip"
);
Console.ReadKey(); Console.ReadKey();
} }

Binary file not shown.

View File

@@ -5,6 +5,7 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>2.0.0</Version>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -23,12 +23,13 @@ public sealed class SafeFileInstaller : IInstaller
var rel = Path.GetRelativePath(sourceDir, src); var rel = Path.GetRelativePath(sourceDir, src);
var dst = Path.Combine(installPath, rel); var dst = Path.Combine(installPath, rel);
var dstDir = Path.GetDirectoryName(dst)!; var dstDir = Path.GetDirectoryName(dst)!;
Directory.CreateDirectory(dstDir); if (!Directory.Exists(dstDir)) Directory.CreateDirectory(dstDir);
if (File.Exists(dst)) if (File.Exists(dst))
{ {
var bkp = Path.Combine(backupDir, rel); var bkp = Path.Combine(backupDir, rel);
Directory.CreateDirectory(Path.GetDirectoryName(bkp)!); var bkpDir = Path.GetDirectoryName(bkp);
if (!Directory.Exists(bkpDir)) Directory.CreateDirectory(bkpDir!);
File.Copy(dst, bkp, overwrite: true); File.Copy(dst, bkp, overwrite: true);
} }
@@ -63,7 +64,8 @@ public sealed class SafeFileInstaller : IInstaller
{ {
var rel = Path.GetRelativePath(backupDir, bkp); var rel = Path.GetRelativePath(backupDir, bkp);
var dst = Path.Combine(installPath, rel); var dst = Path.Combine(installPath, rel);
Directory.CreateDirectory(Path.GetDirectoryName(dst)!); var dstDir = Path.GetDirectoryName(dst);
if (!Directory.Exists(dstDir)) Directory.CreateDirectory(dstDir!);
File.Copy(bkp, dst, overwrite: true); File.Copy(bkp, dst, overwrite: true);
} }
} }

View File

@@ -12,6 +12,7 @@ public sealed class UpdaterApp
private readonly IInstaller _installer; private readonly IInstaller _installer;
private readonly IProcessManager _proc; private readonly IProcessManager _proc;
/// <inheritdoc/>
public UpdaterApp(ILogger log, IExtractor extractor, IInstaller installer, IProcessManager proc) public UpdaterApp(ILogger log, IExtractor extractor, IInstaller installer, IProcessManager proc)
{ {
_log = log; _log = log;
@@ -34,6 +35,7 @@ public sealed class UpdaterApp
{ {
_log.Error($"Extraction failed: {ex.Message}"); _log.Error($"Extraction failed: {ex.Message}");
Cleanup(tempExtractDir); Cleanup(tempExtractDir);
_proc.StartApp(opts.InstallPath, opts.AppExe, opts.RestartDelayMs);
return ExitCodes.ExtractFailed; return ExitCodes.ExtractFailed;
} }
@@ -45,6 +47,7 @@ public sealed class UpdaterApp
{ {
_log.Error($"Install failed: {ex.Message}"); _log.Error($"Install failed: {ex.Message}");
Cleanup(tempExtractDir); Cleanup(tempExtractDir);
_proc.StartApp(opts.InstallPath, opts.AppExe, opts.RestartDelayMs);
return ExitCodes.InstallFailed; return ExitCodes.InstallFailed;
} }

View File

@@ -0,0 +1,8 @@
{
"profiles": {
"Updater": {
"commandName": "Project",
"commandLineArgs": "-z \"C:\\Job\\Projects\\FrigaT\\ReleaseUpdater\\Updater.Test\\bin\\Debug\\net8.0\\Tools\\tempUpdater\\updater_32f606dc1cda473ab953206927bf047b.zip\" -i \"C:\\Job\\Projects\\FrigaT\\ReleaseUpdater\\Updater.Test\\bin\\Debug\\net8.0\" -a \"RetailUpdatesBot.exe\" -rd \"500\" -ud \"500\" -wp \"31408\""
}
}
}

View File

@@ -5,6 +5,17 @@
<TargetFramework>net8.0</TargetFramework> <TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<Version>2.0.0</Version>
<Authors>FrigaT</Authors>
<Company>FrigaT</Company>
<Product>ReleaseUpdater</Product>
<Description>Запускатор обновления</Description>
<Copyright>Copyright © 2025 FrigaT</Copyright>
<RepositoryUrl>https://git.frigat.duckdns.org/FrigaT/ReleaseUpdater</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<PackageProjectUrl>https://git.frigat.duckdns.org/FrigaT/ReleaseUpdater</PackageProjectUrl>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>