diff --git a/src/ElectronicPharmacy/App.config b/src/ElectronicPharmacy/App.config
index 40c70be..cc6c7be 100644
--- a/src/ElectronicPharmacy/App.config
+++ b/src/ElectronicPharmacy/App.config
@@ -51,6 +51,9 @@
+
+ True
+
Электронная_Фармация.Properties.Settings>
\ No newline at end of file
diff --git a/src/ElectronicPharmacy/Classes/AppConfig.cs b/src/ElectronicPharmacy/Classes/AppConfig.cs
index 6fa57c2..4bf6a89 100644
--- a/src/ElectronicPharmacy/Classes/AppConfig.cs
+++ b/src/ElectronicPharmacy/Classes/AppConfig.cs
@@ -20,6 +20,27 @@ namespace Электронная_Фармация.Classes
///
/// Replaces known dead API URLs in saved user settings.
///
+ ///
+ /// Carries user settings across AssemblyVersion bumps (new user.config folder).
+ ///
+ public static void UpgradeUserSettingsIfNeeded()
+ {
+ try
+ {
+ if (Settings.Default.UpgradeSettings)
+ {
+ Settings.Default.Upgrade();
+ Settings.Default.UpgradeSettings = false;
+ Settings.Default.Save();
+ AppDebugLog.Info("AppConfig", "User settings upgraded from previous version");
+ }
+ }
+ catch (Exception ex)
+ {
+ AppDebugLog.Info("AppConfig", $"Upgrade settings skipped: {ex.Message}");
+ }
+ }
+
public static void MigrateLegacySettings()
{
var current = Settings.Default.ApiBaseUrl;
diff --git a/src/ElectronicPharmacy/Classes/AutoUpdater.cs b/src/ElectronicPharmacy/Classes/AutoUpdater.cs
new file mode 100644
index 0000000..80c2965
--- /dev/null
+++ b/src/ElectronicPharmacy/Classes/AutoUpdater.cs
@@ -0,0 +1,192 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.IO.Compression;
+using System.Linq;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace Электронная_Фармация.Classes
+{
+ ///
+ /// Downloads a Gitea release ZIP and applies it via an external batch updater
+ /// so the running exe can be replaced. Preserves local databases and Logs.
+ ///
+ public static class AutoUpdater
+ {
+ public static async Task ApplyUpdateAsync(
+ UpdateCheckResult update,
+ IProgress status = null)
+ {
+ if (update == null)
+ {
+ throw new ArgumentNullException(nameof(update));
+ }
+
+ var downloadUrl = !string.IsNullOrWhiteSpace(update.DownloadUrl)
+ ? update.DownloadUrl
+ : update.ReleaseUrl;
+ if (string.IsNullOrWhiteSpace(downloadUrl) ||
+ !downloadUrl.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
+ {
+ // Prefer zipball if asset missing
+ if (!string.IsNullOrWhiteSpace(downloadUrl) &&
+ !downloadUrl.EndsWith(".zip", StringComparison.OrdinalIgnoreCase))
+ {
+ throw new InvalidOperationException(
+ "В релизе нет ZIP-сборки. Прикрепите ElectronicPharmacy-vX.Y.Z.zip к Release.");
+ }
+
+ throw new InvalidOperationException("Нет ссылки на ZIP обновления.");
+ }
+
+ var installDir = AppDomain.CurrentDomain.BaseDirectory.TrimEnd(
+ Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
+ var currentExe = Process.GetCurrentProcess().MainModule?.FileName;
+ if (string.IsNullOrWhiteSpace(currentExe) || !File.Exists(currentExe))
+ {
+ currentExe = Directory.GetFiles(installDir, "*.exe")
+ .FirstOrDefault(p => !p.EndsWith(".vshost.exe", StringComparison.OrdinalIgnoreCase));
+ }
+
+ if (string.IsNullOrWhiteSpace(currentExe))
+ {
+ throw new InvalidOperationException("Не найден текущий exe.");
+ }
+
+ var workRoot = Path.Combine(Path.GetTempPath(), "elfisa-update-" + Guid.NewGuid().ToString("N"));
+ var zipPath = Path.Combine(workRoot, "update.zip");
+ var extractDir = Path.Combine(workRoot, "extract");
+ Directory.CreateDirectory(extractDir);
+
+ try
+ {
+ Report(status, "Скачивание обновления...");
+ await DownloadAsync(downloadUrl, zipPath).ConfigureAwait(false);
+
+ Report(status, "Распаковка...");
+ ZipFile.ExtractToDirectory(zipPath, extractDir);
+
+ var payloadDir = FindPayloadDirectory(extractDir);
+ if (payloadDir == null)
+ {
+ throw new InvalidOperationException("В архиве не найден exe приложения.");
+ }
+
+ var updaterBat = Path.Combine(workRoot, "apply-update.cmd");
+ WriteUpdaterScript(updaterBat);
+
+ Report(status, "Подготовка перезапуска...");
+ var psi = new ProcessStartInfo
+ {
+ FileName = updaterBat,
+ Arguments = Quote(payloadDir) + " " + Quote(installDir) + " " +
+ Quote(Path.GetFileName(currentExe)) + " " +
+ Process.GetCurrentProcess().Id,
+ UseShellExecute = true,
+ WindowStyle = ProcessWindowStyle.Hidden,
+ WorkingDirectory = workRoot,
+ CreateNoWindow = true
+ };
+ Process.Start(psi);
+ AppDebugLog.Info("AutoUpdater",
+ $"Updater started. payload={payloadDir}, install={installDir}, ver={update.LatestVersion}");
+ }
+ catch
+ {
+ try
+ {
+ if (Directory.Exists(workRoot))
+ {
+ Directory.Delete(workRoot, true);
+ }
+ }
+ catch
+ {
+ // ignore cleanup errors
+ }
+
+ throw;
+ }
+ }
+
+ private static async Task DownloadAsync(string url, string zipPath)
+ {
+ using (var client = new HttpClient { Timeout = TimeSpan.FromMinutes(10) })
+ {
+ var token = AppConfig.UpdateApiToken;
+ if (!string.IsNullOrWhiteSpace(token))
+ {
+ client.DefaultRequestHeaders.Authorization =
+ new AuthenticationHeaderValue("token", token.Trim());
+ }
+
+ using (var response = await client.GetAsync(url, HttpCompletionOption.ResponseHeadersRead)
+ .ConfigureAwait(false))
+ {
+ response.EnsureSuccessStatusCode();
+ using (var fs = new FileStream(zipPath, FileMode.Create, FileAccess.Write, FileShare.None))
+ {
+ await response.Content.CopyToAsync(fs).ConfigureAwait(false);
+ }
+ }
+ }
+ }
+
+ private static string FindPayloadDirectory(string extractRoot)
+ {
+ // ZIP may contain a single root folder (Compress-Archive of a directory).
+ var exe = Directory.GetFiles(extractRoot, "*.exe", SearchOption.AllDirectories)
+ .FirstOrDefault(p =>
+ {
+ var name = Path.GetFileName(p) ?? string.Empty;
+ return !name.EndsWith(".vshost.exe", StringComparison.OrdinalIgnoreCase) &&
+ !name.Equals("UpdateCheckSmoke.exe", StringComparison.OrdinalIgnoreCase);
+ });
+
+ return exe == null ? null : Path.GetDirectoryName(exe);
+ }
+
+ private static void WriteUpdaterScript(string path)
+ {
+ // Waits for app exit, copies files, keeps local DB/Logs, restarts exe.
+ var sb = new StringBuilder();
+ sb.AppendLine("@echo off");
+ sb.AppendLine("setlocal");
+ sb.AppendLine("set \"SRC=%~1\"");
+ sb.AppendLine("set \"DST=%~2\"");
+ sb.AppendLine("set \"EXENAME=%~3\"");
+ sb.AppendLine("set \"PID=%~4\"");
+ sb.AppendLine("echo Waiting for PID %PID% ... > \"%TEMP%\\elfisa-updater.log\"");
+ sb.AppendLine(":waitloop");
+ sb.AppendLine("tasklist /FI \"PID eq %PID%\" 2>nul | find \"%PID%\" >nul");
+ sb.AppendLine("if not errorlevel 1 (");
+ sb.AppendLine(" timeout /t 1 /nobreak >nul");
+ sb.AppendLine(" goto waitloop");
+ sb.AppendLine(")");
+ sb.AppendLine("timeout /t 1 /nobreak >nul");
+ sb.AppendLine("echo Copying update... >> \"%TEMP%\\elfisa-updater.log\"");
+ // /E copy tree; exclude local DB and logs from being overwritten/removed.
+ sb.AppendLine("robocopy \"%SRC%\" \"%DST%\" /E /R:2 /W:1 /NFL /NDL /NJH /NJS /XF efClient.db *.db /XD Logs");
+ sb.AppendLine("set \"RC=%ERRORLEVEL%\"");
+ sb.AppendLine("echo robocopy=%RC% >> \"%TEMP%\\elfisa-updater.log\"");
+ // robocopy 0-7 = success-ish
+ sb.AppendLine("start \"\" /D \"%DST%\" \"%DST%\\%EXENAME%\"");
+ sb.AppendLine("exit /b 0");
+ File.WriteAllText(path, sb.ToString(), Encoding.Default);
+ }
+
+ private static string Quote(string value)
+ {
+ return "\"" + (value ?? string.Empty).Replace("\"", string.Empty) + "\"";
+ }
+
+ private static void Report(IProgress status, string message)
+ {
+ AppDebugLog.Info("AutoUpdater", message);
+ status?.Report(message);
+ }
+ }
+}
diff --git a/src/ElectronicPharmacy/ElectronicPharmacy.csproj b/src/ElectronicPharmacy/ElectronicPharmacy.csproj
index 176bf79..c3750a0 100644
--- a/src/ElectronicPharmacy/ElectronicPharmacy.csproj
+++ b/src/ElectronicPharmacy/ElectronicPharmacy.csproj
@@ -98,6 +98,8 @@
+
+
@@ -106,6 +108,7 @@
+
diff --git a/src/ElectronicPharmacy/HelpForms/HF_UpdateAvailable.cs b/src/ElectronicPharmacy/HelpForms/HF_UpdateAvailable.cs
index 85b762a..06c7b89 100644
--- a/src/ElectronicPharmacy/HelpForms/HF_UpdateAvailable.cs
+++ b/src/ElectronicPharmacy/HelpForms/HF_UpdateAvailable.cs
@@ -1,5 +1,5 @@
using System;
-using System.Diagnostics;
+using System.Threading.Tasks;
using System.Windows.Forms;
using Электронная_Фармация.Classes;
@@ -8,6 +8,7 @@ namespace Электронная_Фармация.HelpForms
public partial class HF_UpdateAvailable : Form
{
private readonly UpdateCheckResult _result;
+ private bool _updating;
public HF_UpdateAvailable(UpdateCheckResult result)
{
@@ -19,44 +20,78 @@ namespace Электронная_Фармация.HelpForms
var local = string.IsNullOrWhiteSpace(_result.LocalVersion) ? "?" : _result.LocalVersion;
lblMessage.Text =
$"Доступно новое обновление {latest} (у вас {local}).{Environment.NewLine}{Environment.NewLine}" +
- "Для продолжения работы необходимо установить новую версию.";
+ "Нажмите «Обновить» — программа скачает и установит новую версию сама." + Environment.NewLine +
+ "Локальная база и данные сохранятся.";
+ btnDownload.Text = "Обновить";
}
- private void btnDownload_Click(object sender, EventArgs e)
+ private async void btnDownload_Click(object sender, EventArgs e)
{
- var url = !string.IsNullOrWhiteSpace(_result.DownloadUrl)
- ? _result.DownloadUrl
- : _result.ReleaseUrl;
- if (string.IsNullOrWhiteSpace(url))
+ if (_updating)
+ {
+ return;
+ }
+
+ if (string.IsNullOrWhiteSpace(_result.DownloadUrl) &&
+ string.IsNullOrWhiteSpace(_result.ReleaseUrl))
{
UiDialogs.ShowError("Ссылка на обновление не задана.", "Обновление", this);
return;
}
+ _updating = true;
+ btnDownload.Enabled = false;
+ btnExit.Enabled = false;
+ ControlBox = false;
+
+ var progress = new Progress(msg =>
+ {
+ lblMessage.Text = msg;
+ });
+
try
{
- Process.Start(new ProcessStartInfo
- {
- FileName = url,
- UseShellExecute = true
- });
+ await AutoUpdater.ApplyUpdateAsync(_result, progress);
+ lblMessage.Text = "Обновление скачано. Перезапуск...";
+ DialogResult = DialogResult.OK;
+ Application.Exit();
}
catch (Exception ex)
{
- UiDialogs.ShowError($"Не удалось открыть ссылку:{Environment.NewLine}{ex.Message}", "Обновление", this);
+ AppDebugLog.Error("Update", "Автообновление не удалось", ex);
+ _updating = false;
+ btnDownload.Enabled = true;
+ btnExit.Enabled = true;
+ ControlBox = true;
+ UiDialogs.ShowError(
+ "Не удалось установить обновление автоматически." + Environment.NewLine +
+ ex.Message,
+ "Обновление",
+ this);
}
}
private void btnExit_Click(object sender, EventArgs e)
{
+ if (_updating)
+ {
+ return;
+ }
+
DialogResult = DialogResult.Abort;
Close();
}
private void HF_UpdateAvailable_FormClosing(object sender, FormClosingEventArgs e)
{
- // Крестик / Alt+F4 — тоже выход из приложения (не пускать в работу).
- if (DialogResult != DialogResult.Abort)
+ if (_updating)
+ {
+ e.Cancel = true;
+ return;
+ }
+
+ // Крестик / Alt+F4 — выход из приложения (не пускать в работу).
+ if (DialogResult != DialogResult.Abort && DialogResult != DialogResult.OK)
{
DialogResult = DialogResult.Abort;
}
diff --git a/src/ElectronicPharmacy/Program.cs b/src/ElectronicPharmacy/Program.cs
index a6db27c..acdf2a2 100644
--- a/src/ElectronicPharmacy/Program.cs
+++ b/src/ElectronicPharmacy/Program.cs
@@ -12,6 +12,7 @@ namespace Электронная_Фармация
{
ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12;
AppConfig.MigrateLegacySettings();
+ AppConfig.UpgradeUserSettingsIfNeeded();
AppDebugLog.Info("App", $"Запуск. ApiBaseUrl={AppConfig.ApiBaseUrl}, Log={AppDebugLog.CurrentLogFilePath}");
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
diff --git a/src/ElectronicPharmacy/Properties/AssemblyInfo.cs b/src/ElectronicPharmacy/Properties/AssemblyInfo.cs
index 17274f1..43a019a 100644
--- a/src/ElectronicPharmacy/Properties/AssemblyInfo.cs
+++ b/src/ElectronicPharmacy/Properties/AssemblyInfo.cs
@@ -32,5 +32,5 @@ using System.Runtime.InteropServices;
//
// "*", :
// [assembly: AssemblyVersion("1.0.*")]
-[assembly: AssemblyVersion("1.0.11.0")]
-[assembly: AssemblyFileVersion("1.0.11.0")]
+[assembly: AssemblyVersion("1.0.12.0")]
+[assembly: AssemblyFileVersion("1.0.12.0")]
diff --git a/src/ElectronicPharmacy/Properties/Settings.Designer.cs b/src/ElectronicPharmacy/Properties/Settings.Designer.cs
index d574351..7d62897 100644
--- a/src/ElectronicPharmacy/Properties/Settings.Designer.cs
+++ b/src/ElectronicPharmacy/Properties/Settings.Designer.cs
@@ -203,5 +203,17 @@ namespace Электронная_Фармация.Properties {
}
}
+ [global::System.Configuration.UserScopedSettingAttribute()]
+ [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
+ [global::System.Configuration.DefaultSettingValueAttribute("True")]
+ public bool UpgradeSettings {
+ get {
+ return ((bool)(this["UpgradeSettings"]));
+ }
+ set {
+ this["UpgradeSettings"] = value;
+ }
+ }
+
}
}
diff --git a/src/ElectronicPharmacy/Properties/Settings.settings b/src/ElectronicPharmacy/Properties/Settings.settings
index 995ccdf..68532f4 100644
--- a/src/ElectronicPharmacy/Properties/Settings.settings
+++ b/src/ElectronicPharmacy/Properties/Settings.settings
@@ -47,5 +47,8 @@
+
+ True
+
\ No newline at end of file