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)