From fdebc16b377425fc606f0c8e0551015dcae958d8 Mon Sep 17 00:00:00 2001 From: kislovdm Date: Mon, 27 Apr 2026 00:32:53 +0300 Subject: [PATCH] releases --- .github/workflows/dotnet-desktop.yml | 1 + .semver.txt | 2 +- src/MonsterMonitor/MonsterMonitor.csproj | 10 +- .../Services/GitHubUpdateService.cs | 332 ++++++++++++++++++ src/MonsterMonitor/UI/MainForm.cs | 11 + 5 files changed, 351 insertions(+), 5 deletions(-) create mode 100644 src/MonsterMonitor/Services/GitHubUpdateService.cs diff --git a/.github/workflows/dotnet-desktop.yml b/.github/workflows/dotnet-desktop.yml index 41bd407..052ed34 100644 --- a/.github/workflows/dotnet-desktop.yml +++ b/.github/workflows/dotnet-desktop.yml @@ -1,4 +1,5 @@ name: .NET Core Desktop +permissions: write-all on: push: diff --git a/.semver.txt b/.semver.txt index 21e8796..ee90284 100644 --- a/.semver.txt +++ b/.semver.txt @@ -1 +1 @@ -1.0.3 +1.0.4 diff --git a/src/MonsterMonitor/MonsterMonitor.csproj b/src/MonsterMonitor/MonsterMonitor.csproj index 51f4246..5bba326 100644 --- a/src/MonsterMonitor/MonsterMonitor.csproj +++ b/src/MonsterMonitor/MonsterMonitor.csproj @@ -6,10 +6,10 @@ 7.3 MonsterMonitor MonsterMonitor - 1.0.3 - 1.0.3 - 1.0.3 - 1.0.3 + 1.0.4 + 1.0.4 + 1.0.4 + 1.0.4 @@ -18,7 +18,9 @@ all + + diff --git a/src/MonsterMonitor/Services/GitHubUpdateService.cs b/src/MonsterMonitor/Services/GitHubUpdateService.cs new file mode 100644 index 0000000..b15c2d8 --- /dev/null +++ b/src/MonsterMonitor/Services/GitHubUpdateService.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.IO.Compression; +using System.Net; +using System.Runtime.Serialization; +using System.Runtime.Serialization.Json; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Forms; + +namespace MonsterMonitor.Services +{ + public sealed class GitHubUpdateService + { + private const string GithubUser = "monster1025"; + private const string GithubRepo = "monster_monitor_v2"; + private const string GithubToken = ""; + + private readonly LogService _log; + + public GitHubUpdateService(LogService log) + { + _log = log; + } + + public async Task CheckAndPrepareUpdateAsync() + { + try + { + var currentVersion = GetCurrentVersion(); + _log.Info("Проверка обновлений..."); + _log.Info("Текущая версия: " + currentVersion); + + var release = await GetLatestReleaseAsync(); + if (release == null) + { + _log.Warn("Не удалось получить данные о релизе с GitHub."); + return; + } + + var latestVersion = ParseVersion(release.TagName); + if (latestVersion == null) + { + _log.Warn("Не удалось распарсить версию релиза: " + (release.TagName ?? "")); + return; + } + + _log.Info("Версия на GitHub: " + latestVersion); + if (latestVersion <= currentVersion) + { + _log.Info("Обновление не требуется."); + return; + } + + _log.Info("Найдена новая версия. Начинаю загрузку обновления..."); + var zipAsset = FindZipAsset(release.Assets); + if (zipAsset == null) + { + _log.Warn("У релиза нет .zip-ассета для автообновления."); + return; + } + + var tempRoot = Path.Combine(Path.GetTempPath(), "MonsterMonitorUpdate"); + var packageDir = Path.Combine(tempRoot, latestVersion.ToString()); + var packageZip = Path.Combine(packageDir, "update.zip"); + var extractDir = Path.Combine(packageDir, "extracted"); + Directory.CreateDirectory(packageDir); + + await DownloadFileAsync(GetAssetDownloadUrl(zipAsset), packageZip); + _log.Info("Архив обновления загружен: " + zipAsset.Name); + + if (Directory.Exists(extractDir)) + { + Directory.Delete(extractDir, true); + } + + ZipFile.ExtractToDirectory(packageZip, extractDir); + _log.Info("Архив обновления распакован."); + + var appDir = AppDomain.CurrentDomain.BaseDirectory; + var exePath = Application.ExecutablePath; + var updaterBat = Path.Combine(packageDir, "apply_update.bat"); + File.WriteAllText(updaterBat, BuildUpdaterScript(appDir, extractDir, exePath), Encoding.ASCII); + + _log.Info("Обновление подготовлено."); + var restartNow = MessageBox.Show( + "Обновление загружено и готово к установке. Перезапустить приложение сейчас?", + "Monster Monitor", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question); + + if (restartNow == DialogResult.Yes) + { + _log.Info("Перезапуск приложения для применения обновления..."); + StartUpdaterAndExit(updaterBat); + return; + } + + _log.Warn("Перезапуск отложен. Обновление будет применено после следующего запуска через обновлятор."); + } + catch (WebException ex) + { + var response = ex.Response as HttpWebResponse; + var statusCode = response != null ? ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture) : "n/a"; + var statusText = response != null ? response.StatusCode.ToString() : "unknown"; + _log.Error("Ошибка обновления (HTTP): " + statusCode + " " + statusText + ". " + ex.Message); + } + catch (Exception ex) + { + _log.Error("Ошибка обновления: " + ex.Message); + } + } + + private static Version GetCurrentVersion() + { + Version version; + if (!Version.TryParse(Application.ProductVersion, out version)) + { + version = new Version(1, 0, 0, 0); + } + + return version; + } + + private static Version ParseVersion(string rawVersion) + { + if (string.IsNullOrWhiteSpace(rawVersion)) + { + return null; + } + + var normalized = rawVersion.Trim(); + if (normalized.StartsWith("v", true, CultureInfo.InvariantCulture)) + { + normalized = normalized.Substring(1); + } + + Version version; + if (Version.TryParse(normalized, out version)) + { + return version; + } + + return null; + } + + private async Task GetLatestReleaseAsync() + { + var url = string.Format( + CultureInfo.InvariantCulture, + "https://api.github.com/repos/{0}/{1}/releases/latest", + GithubUser, + GithubRepo); + + try + { + var request = CreateGithubRequest(url, "application/vnd.github+json"); + using (var response = (HttpWebResponse)await request.GetResponseAsync()) + using (var stream = response.GetResponseStream()) + { + if (stream == null) + { + return null; + } + + var serializer = new DataContractJsonSerializer(typeof(GithubRelease)); + return serializer.ReadObject(stream) as GithubRelease; + } + } + catch (WebException ex) + { + var response = ex.Response as HttpWebResponse; + if (response != null && response.StatusCode == HttpStatusCode.NotFound) + { + _log.Warn("Релиз /latest не найден. Пробую получить список релизов..."); + return await GetLatestReleaseFromListAsync(); + } + + throw; + } + } + + private async Task GetLatestReleaseFromListAsync() + { + var url = string.Format( + CultureInfo.InvariantCulture, + "https://api.github.com/repos/{0}/{1}/releases", + GithubUser, + GithubRepo); + + var request = CreateGithubRequest(url, "application/vnd.github+json"); + using (var response = (HttpWebResponse)await request.GetResponseAsync()) + using (var stream = response.GetResponseStream()) + { + if (stream == null) + { + return null; + } + + var serializer = new DataContractJsonSerializer(typeof(List)); + var releases = serializer.ReadObject(stream) as List; + if (releases == null || releases.Count == 0) + { + return null; + } + + return releases[0]; + } + } + + private async Task DownloadFileAsync(string url, string filePath) + { + var request = CreateGithubRequest(url, "application/octet-stream"); + + using (var response = (HttpWebResponse)await request.GetResponseAsync()) + using (var responseStream = response.GetResponseStream()) + using (var fileStream = File.Create(filePath)) + { + if (responseStream == null) + { + throw new InvalidOperationException("Пустой поток ответа при загрузке файла."); + } + + await responseStream.CopyToAsync(fileStream); + } + } + + private static GithubAsset FindZipAsset(IList assets) + { + if (assets == null) + { + return null; + } + + foreach (var asset in assets) + { + if (asset == null || string.IsNullOrWhiteSpace(asset.Name)) + { + continue; + } + + if (asset.Name.EndsWith(".zip", StringComparison.OrdinalIgnoreCase)) + { + return asset; + } + } + + return null; + } + + private static string GetAssetDownloadUrl(GithubAsset asset) + { + if (asset == null) + { + return string.Empty; + } + + if (!string.IsNullOrWhiteSpace(asset.ApiUrl)) + { + return asset.ApiUrl; + } + + return asset.DownloadUrl ?? string.Empty; + } + + private static HttpWebRequest CreateGithubRequest(string url, string accept) + { + var request = (HttpWebRequest)WebRequest.Create(url); + request.Method = "GET"; + request.Accept = accept; + request.UserAgent = "MonsterMonitorUpdater/1.0"; + request.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate; + if (!string.IsNullOrWhiteSpace(GithubToken)) + { + request.Headers.Add("Authorization", "Bearer " + GithubToken); + } + + return request; + } + + private static string BuildUpdaterScript(string appDir, string extractDir, string exePath) + { + return "@echo off" + Environment.NewLine + + "setlocal" + Environment.NewLine + + "timeout /t 2 /nobreak >nul" + Environment.NewLine + + "xcopy \"" + extractDir + "\\*\" \"" + appDir + "\\\" /E /I /Y /Q >nul" + Environment.NewLine + + "start \"\" \"" + exePath + "\"" + Environment.NewLine + + "endlocal" + Environment.NewLine; + } + + private static void StartUpdaterAndExit(string updaterBat) + { + var psi = new ProcessStartInfo + { + FileName = updaterBat, + WorkingDirectory = Path.GetDirectoryName(updaterBat) ?? AppDomain.CurrentDomain.BaseDirectory, + UseShellExecute = true, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden + }; + + Process.Start(psi); + Application.Exit(); + } + } + + [DataContract] + public sealed class GithubRelease + { + [DataMember(Name = "tag_name")] + public string TagName { get; set; } + + [DataMember(Name = "assets")] + public List Assets { get; set; } + } + + [DataContract] + public sealed class GithubAsset + { + [DataMember(Name = "url")] + public string ApiUrl { get; set; } + + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "browser_download_url")] + public string DownloadUrl { get; set; } + } +} diff --git a/src/MonsterMonitor/UI/MainForm.cs b/src/MonsterMonitor/UI/MainForm.cs index 4337ee0..caa434b 100644 --- a/src/MonsterMonitor/UI/MainForm.cs +++ b/src/MonsterMonitor/UI/MainForm.cs @@ -1,5 +1,6 @@ using System; using System.Drawing; +using System.Threading.Tasks; using System.Windows.Forms; using MonsterMonitor.Models; using MonsterMonitor.Services; @@ -15,6 +16,7 @@ namespace MonsterMonitor.UI private readonly Icon _trayIcon; private readonly LogService _log = new LogService(); private readonly PowerManagementService _power = new PowerManagementService(); + private GitHubUpdateService _updateService; private AppController _controller; private AppSettings _settings; private bool _allowClose; @@ -39,7 +41,9 @@ namespace MonsterMonitor.UI _settings = AppSettings.Load(); _controller = new AppController(_log, _power); + _updateService = new GitHubUpdateService(_log); RestartServices(); + _ = RunUpdateCheckAsync(); } private void BuildUi() @@ -164,6 +168,13 @@ namespace MonsterMonitor.UI _console.ScrollToCaret(); } + private async Task RunUpdateCheckAsync() + { + // Небольшая задержка, чтобы окно и лог уже успели инициализироваться. + await Task.Delay(1000); + await _updateService.CheckAndPrepareUpdateAsync(); + } + private static Color GetColor(LogLevel level) { switch (level)