diff --git a/ReleaseUpdater/ReleaseProvider.cs b/ReleaseUpdater/ReleaseProvider.cs index 00a7f88..3fe117c 100644 --- a/ReleaseUpdater/ReleaseProvider.cs +++ b/ReleaseUpdater/ReleaseProvider.cs @@ -17,7 +17,7 @@ public sealed class ReleaseProvider /// Токен авторизации (если требуется). /// Список релизов. - public async Task> GetReleasesAsync(string apiUrl, string? token = null) + public async Task> GetReleasesAsync(Uri apiUrl, string? token = null) { using var client = CreateClient(token); using var resp = await client.GetAsync(apiUrl); @@ -38,7 +38,7 @@ public sealed class ReleaseProvider /// Токен авторизации (если требуется). /// Информация о релизе или null. - public async Task FindReleaseAsync(string apiUrl, string? versionOrLatest, string? token = null) + public async Task FindReleaseAsync(Uri apiUrl, string? versionOrLatest, string? token = null) { var all = await GetReleasesAsync(apiUrl, token); if (string.IsNullOrWhiteSpace(versionOrLatest) || versionOrLatest.Equals("latest", StringComparison.OrdinalIgnoreCase)) diff --git a/ReleaseUpdater/ReleaseUpdaterFacade.cs b/ReleaseUpdater/ReleaseUpdaterFacade.cs index 1736d7a..7249d96 100644 --- a/ReleaseUpdater/ReleaseUpdaterFacade.cs +++ b/ReleaseUpdater/ReleaseUpdaterFacade.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO.Compression; +using System.Reflection; namespace ReleaseUpdater; @@ -14,16 +15,16 @@ public static class ReleaseUpdaterFacade /// public static event Action? BeforeInstall; - /// - /// Событие вызывается после успешной установки новой версии. - /// - public static event Action? AfterInstall; - /// /// Событие вызывается при ошибке обновления. /// public static event Action? UpdateFailed; + /// + /// Событие вызывается, если текущая версия совпадает с требуемой. + /// + public static event Action? AlreadyUp; + /// /// Получает список доступных версий из Gitea. /// @@ -34,45 +35,27 @@ public static class ReleaseUpdaterFacade return releases.Select(r => r.TagName).ToList(); } - /// - /// Обновление без Updater.exe: скачивание, распаковка и перезапуск прямо из DLL. - /// - 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); - } - } - /// /// Обновление через внешний Updater.exe. /// - public static async Task UpdateWithExternalAsync( - string apiUrl, string? token, string installPath, string appExe, string versionOrLatest = "latest", string? updaterExePath = null, bool exitCurrentApp = false, string? tempUpdaterDirectory = null) + /// API github/gitea release + /// Token авторизации + /// Путь для установки приложения + /// Наименование файла приложения + /// Папка для временного хранилища zip архива + /// Путь к updater.exe + /// Тэг с версией / "latest" + /// Маска наименовая ассета обновления. В маске может содержаться {version} + public static async Task UpdateAsync( + Uri apiUrl, + string? token, + string installPath, + string appExeName, + string tempDirectory, + string updaterExePath, + string versionOrLatest = "latest", + string? assetMask = null + ) { try { @@ -80,32 +63,42 @@ public static class ReleaseUpdaterFacade 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 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 (updaterExePath == null) updaterExePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Updater.exe"); - - if (string.IsNullOrWhiteSpace(tempUpdaterDirectory)) + if (string.IsNullOrWhiteSpace(tempDirectory)) { - tempUpdaterDirectory = Path.GetDirectoryName(updaterExePath); - tempUpdaterDirectory = Path.Combine(tempUpdaterDirectory!, "tempUpdater"); + tempDirectory = Path.GetTempPath(); + } + else if (!Directory.Exists(tempDirectory)) + { + Directory.CreateDirectory(tempDirectory); } - if (!Directory.Exists(tempUpdaterDirectory)) - { - Directory.CreateDirectory(tempUpdaterDirectory); - } - - var tempUpdaterPath = Path.Combine(tempUpdaterDirectory, tempUpdaterName!); - + var tempUpdaterPath = Path.Combine(tempDirectory, tempUpdaterName); var downloader = new HttpAssetDownloader(); - var zipPath = await downloader.DownloadAssetAsync(asset.DownloadUrl, token, Path.Combine(tempUpdaterDirectory, $"updater_{tempNumber}.zip")); + var zipPath = await downloader.DownloadAssetAsync(asset.DownloadUrl, token, Path.Combine(tempDirectory, $"updater_{tempNumber}.zip")); - BeforeInstall?.Invoke(); File.Copy(updaterExePath, tempUpdaterPath); @@ -118,17 +111,16 @@ public static class ReleaseUpdaterFacade { ZipPath = zipPath, InstallPath = installPath, - AppExe = appExe, + AppExe = appExeName, }; - if (exitCurrentApp) - { - int pid = Process.GetCurrentProcess().Id; - updaterOptions.WaitProcess = pid; - } + int pid = Process.GetCurrentProcess().Id; + updaterOptions.WaitProcess = pid; var args = ArgumentsToolkit.ArgumentsParser.ToArguments(updaterOptions, true); + BeforeInstall?.Invoke(); + var process = Process.Start(new ProcessStartInfo { FileName = tempUpdaterPath, @@ -137,36 +129,24 @@ public static class ReleaseUpdaterFacade 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); } catch (Exception ex) { UpdateFailed?.Invoke(ex); - RestartCurrent(installPath, appExe); } } - private static void RestartCurrent(string installPath, string appExe) + /// + /// Получение текущей версии приложения + /// + /// + public static string GetCurrentVersion() { - var currentApp = Path.Combine(installPath, appExe); - if (File.Exists(currentApp)) - { - Process.Start(new ProcessStartInfo - { - FileName = currentApp, - UseShellExecute = true, - WorkingDirectory = installPath - }); - } + var entryAssembly = Assembly.GetEntryAssembly(); + var attr = entryAssembly?.GetCustomAttribute(); + return attr?.InformationalVersion + ?? entryAssembly?.GetName().Version?.ToString().Split("+")[0] + ?? "unknown"; } - } diff --git a/ReleaseUpdater/SemVerService.cs b/ReleaseUpdater/SemVerService.cs index 50ca57a..1db374b 100644 --- a/ReleaseUpdater/SemVerService.cs +++ b/ReleaseUpdater/SemVerService.cs @@ -11,7 +11,7 @@ public sealed class SemVerService /// Строка версии (например, "3.5.2"). /// Результат парсинга. /// true, если парсинг успешен. - public bool TryParse(string version, out Version parsed) + public static bool TryParse(string version, out Version parsed) { var v = version.Trim().TrimStart('v'); return Version.TryParse(Normalize(v), out parsed); @@ -34,7 +34,7 @@ public sealed class SemVerService /// Первая версия. /// Вторая версия. /// -1 если v1 < v2, 0 если равны, 1 если v1 > v2. - public int Compare(string v1, string v2) + public static int Compare(string v1, string v2) { TryParse(v1, out var a); TryParse(v2, out var b); diff --git a/Updater.Test/Program.cs b/Updater.Test/Program.cs index ff6c6f7..61bda39 100644 --- a/Updater.Test/Program.cs +++ b/Updater.Test/Program.cs @@ -14,7 +14,22 @@ internal class Program var url = "https://git.frigat.duckdns.org/api/v1/repos/automacon/RetailUpdatesBot/releases"; 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())); + + await ReleaseUpdaterFacade.UpdateAsync( + apiUrl: url, + token: APIKey, + installPath: installPath, + appExeName: appExe, + tempDirectory: Path.Combine(updaterPath, "Tools", "Temp"), + updaterExePath: updaterPath, + versionOrLatest: "latest", + assetMask: "RetailUpdatesBot-{version}.zip" + ); Console.ReadKey(); } diff --git a/Updater.Test/Tools/Updater.exe b/Updater.Test/Tools/Updater.exe index 8b30a80..2d822dc 100644 Binary files a/Updater.Test/Tools/Updater.exe and b/Updater.Test/Tools/Updater.exe differ diff --git a/Updater.Test/Updater.Test.csproj b/Updater.Test/Updater.Test.csproj index da8130a..1d8002e 100644 --- a/Updater.Test/Updater.Test.csproj +++ b/Updater.Test/Updater.Test.csproj @@ -5,6 +5,7 @@ net8.0 enable enable + 2.0.0 diff --git a/Updater/Core/SafeFileInstaller.cs b/Updater/Core/SafeFileInstaller.cs index 50edf60..7188952 100644 --- a/Updater/Core/SafeFileInstaller.cs +++ b/Updater/Core/SafeFileInstaller.cs @@ -23,12 +23,13 @@ public sealed class SafeFileInstaller : IInstaller var rel = Path.GetRelativePath(sourceDir, src); var dst = Path.Combine(installPath, rel); var dstDir = Path.GetDirectoryName(dst)!; - Directory.CreateDirectory(dstDir); + if (!Directory.Exists(dstDir)) Directory.CreateDirectory(dstDir); if (File.Exists(dst)) { 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); } @@ -63,7 +64,8 @@ public sealed class SafeFileInstaller : IInstaller { var rel = Path.GetRelativePath(backupDir, bkp); 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); } } diff --git a/Updater/Core/UpdaterApp.cs b/Updater/Core/UpdaterApp.cs index b4de7c5..3ea853d 100644 --- a/Updater/Core/UpdaterApp.cs +++ b/Updater/Core/UpdaterApp.cs @@ -12,6 +12,7 @@ public sealed class UpdaterApp private readonly IInstaller _installer; private readonly IProcessManager _proc; + /// public UpdaterApp(ILogger log, IExtractor extractor, IInstaller installer, IProcessManager proc) { _log = log; @@ -34,6 +35,7 @@ public sealed class UpdaterApp { _log.Error($"Extraction failed: {ex.Message}"); Cleanup(tempExtractDir); + _proc.StartApp(opts.InstallPath, opts.AppExe, opts.RestartDelayMs); return ExitCodes.ExtractFailed; } @@ -45,6 +47,7 @@ public sealed class UpdaterApp { _log.Error($"Install failed: {ex.Message}"); Cleanup(tempExtractDir); + _proc.StartApp(opts.InstallPath, opts.AppExe, opts.RestartDelayMs); return ExitCodes.InstallFailed; } diff --git a/Updater/Properties/launchSettings.json b/Updater/Properties/launchSettings.json new file mode 100644 index 0000000..3b6b9cb --- /dev/null +++ b/Updater/Properties/launchSettings.json @@ -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\"" + } + } +} \ No newline at end of file diff --git a/Updater/Updater.csproj b/Updater/Updater.csproj index 67d8837..5e4e07e 100644 --- a/Updater/Updater.csproj +++ b/Updater/Updater.csproj @@ -6,7 +6,7 @@ enable enable true - 1.0.0 + 2.0.0 FrigaT FrigaT ReleaseUpdater