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 (max ~30s, then force-kill), copies files, keeps DB/Logs, restarts.
var sb = new StringBuilder();
sb.AppendLine("@echo off");
sb.AppendLine("setlocal EnableDelayedExpansion");
sb.AppendLine("set \"SRC=%~1\"");
sb.AppendLine("set \"DST=%~2\"");
sb.AppendLine("set \"EXENAME=%~3\"");
sb.AppendLine("set \"PID=%~4\"");
sb.AppendLine("set \"N=0\"");
sb.AppendLine("echo Waiting for PID %PID% ... > \"%TEMP%\\elfisa-updater.log\"");
sb.AppendLine(":waitloop");
sb.AppendLine("tasklist /FI \"PID eq %PID%\" 2>nul | findstr /C:\" %PID% \" >nul");
sb.AppendLine("if errorlevel 1 goto donewait");
sb.AppendLine("set /a N+=1");
sb.AppendLine("echo still alive n=!N! >> \"%TEMP%\\elfisa-updater.log\"");
sb.AppendLine("if !N! GEQ 30 (");
sb.AppendLine(" echo force kill %PID% >> \"%TEMP%\\elfisa-updater.log\"");
sb.AppendLine(" taskkill /PID %PID% /F >nul 2>&1");
sb.AppendLine(" goto donewait");
sb.AppendLine(")");
// ping instead of timeout — timeout often hangs in hidden windows
sb.AppendLine("ping 127.0.0.1 -n 2 >nul");
sb.AppendLine("goto waitloop");
sb.AppendLine(":donewait");
sb.AppendLine("ping 127.0.0.1 -n 2 >nul");
sb.AppendLine("echo Copying update... >> \"%TEMP%\\elfisa-updater.log\"");
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\"");
sb.AppendLine("echo Starting \"%DST%\\%EXENAME%\" >> \"%TEMP%\\elfisa-updater.log\"");
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);
}
}
}