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); } } }