Добавьте файлы проекта.

This commit is contained in:
2025-11-25 07:39:25 +03:00
parent ed6a7e1938
commit 5bbcfb1e76
21 changed files with 793 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
namespace Updater.Core;
public sealed class ConsoleLogger : ILogger
{
public void Info(string message) => Console.WriteLine(message);
public void Warn(string message) => Console.WriteLine("[WARN] " + message);
public void Error(string message) => Console.Error.WriteLine("[ERR] " + message);
}

View File

@@ -0,0 +1,10 @@
namespace Updater.Core;
/// <summary>Извлекает содержимое архива в каталог.</summary>
public interface IExtractor
{
/// <summary>Извлекает архив в целевой каталог.</summary>
/// <param name="archivePath">Путь к ZIP-файлу.</param>
/// <param name="targetDir">Целевой каталог для извлечения.</param>
void Extract(string archivePath, string targetDir);
}

View File

@@ -0,0 +1,12 @@
namespace Updater.Core;
/// <summary>Безопасно устанавливает извлеченные файлы в каталог установки..</summary>
public interface IInstaller
{
/// <summary>
/// Копирует файлы из источника для установки. Путь с резервным копированием и откатом..
/// </summary>
/// <param name="sourceDir">Каталог извлеченного содержимого.</param>
/// <param name="installPath">Целевой каталог установки.</param>
void Install(string sourceDir, string installPath);
}

12
Updater/Core/ILogger.cs Normal file
View File

@@ -0,0 +1,12 @@
namespace Updater.Core;
/// <summary>Простая абстракция журналирования.</summary>
public interface ILogger
{
/// <summary>Информационное сообщение.</summary>
void Info(string message);
/// <summary>Предупреждающее сообщение.</summary>
void Warn(string message);
/// <summary>Сообщение об ошибке.</summary>
void Error(string message);
}

View File

@@ -0,0 +1,8 @@
namespace Updater.Core;
/// <summary>Управляет перезапуском приложения.</summary>
public interface IProcessManager
{
/// <summary>Запускает исполняемый файл приложения из рабочего каталога.</summary>
void StartApp(string installPath, string appExe, int delayMs);
}

66
Updater/Core/Options.cs Normal file
View File

@@ -0,0 +1,66 @@
namespace Updater.Core;
/// <summary>Параметры CLI для Updater.exe.</summary>
public sealed class Options
{
/// <summary>Путь к скачанному архиву (.zip).</summary>
public required string ZipPath { get; init; }
/// <summary>Целевой каталог установки.</summary>
public required string InstallPath { get; init; }
/// <summary>Имя исполняемого файла приложения для перезапуска (e.g., MyBot.exe).</summary>
public required string AppExe { get; init; }
/// <summary>Необязательно: подождите миллисекунды перед перезапуском (льготный период).</summary>
public int RestartDelayMs { get; init; } = 500;
public static string Usage =>
"Usage: Updater.exe --zip <path.zip> --installPath <dir> --appExe <file.exe> [--restartDelayMs <int>]";
/// <summary>Папрсинг CLI аргументов в Options.</summary>
public static Options Parse(string[] args)
{
var dict = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
for (int i = 0; i < args.Length; i++)
{
if (!args[i].StartsWith("--")) continue;
var key = args[i][2..];
var val = (i + 1 < args.Length && !args[i + 1].StartsWith("--")) ? args[i + 1] : "true";
dict[key] = val;
}
var zip = Require(dict, "zip");
var install = Require(dict, "installPath");
var exe = Require(dict, "appExe");
if (!File.Exists(zip)) throw new FileNotFoundException("Zip not found", zip);
Directory.CreateDirectory(install);
return new Options
{
ZipPath = Path.GetFullPath(zip),
InstallPath = Path.GetFullPath(install),
AppExe = exe,
RestartDelayMs = dict.TryGetValue("restartDelayMs", out var d) && int.TryParse(d, out var n) ? n : 500
};
}
private static string Require(IDictionary<string, string> dict, string key)
=> dict.TryGetValue(key, out var v) && !string.IsNullOrWhiteSpace(v)
? v : throw new ArgumentException($"Missing --{key}");
}
public static class ExitCodes
{
/// <summary>Успешное обновление.</summary>
public const int Ok = 0;
/// <summary>Неверные аргументы командной строки.</summary>
public const int InvalidArgs = 2;
/// <summary>Ошибка извлечения.</summary>
public const int ExtractFailed = 3;
/// <summary>Ошибка установки (копировать/заменить).</summary>
public const int InstallFailed = 4;
/// <summary>Ошибка перезапуска.</summary>
public const int RestartFailed = 5;
}

View File

@@ -0,0 +1,30 @@
using System.Diagnostics;
namespace Updater.Core;
public sealed class ProcessManager : IProcessManager
{
private readonly ILogger _log;
public ProcessManager(ILogger log) => _log = log;
public void StartApp(string installPath, string appExe, int delayMs)
{
var appPath = Path.Combine(installPath, appExe);
if (!File.Exists(appPath))
throw new FileNotFoundException("Executable not found after install", appPath);
if (delayMs > 0)
{
_log.Info($"Waiting {delayMs} ms before restart...");
Thread.Sleep(delayMs);
}
_log.Info($"Starting '{appExe}'...");
Process.Start(new ProcessStartInfo
{
FileName = appPath,
UseShellExecute = true,
WorkingDirectory = installPath
});
}
}

View File

@@ -0,0 +1,75 @@
namespace Updater.Core;
/// <summary>
/// Безопасный установщик: создает резервную копию замененных файлов, выполняет атомарную замену,
/// и откатываемся в случае неудачи.
/// </summary>
public sealed class SafeFileInstaller : IInstaller
{
private readonly ILogger _log;
public SafeFileInstaller(ILogger log) => _log = log;
public void Install(string sourceDir, string installPath)
{
var backupDir = Path.Combine(Path.GetTempPath(), $"upd_backup_{Guid.NewGuid():N}");
Directory.CreateDirectory(backupDir);
var completed = false;
try
{
// Копирование файлов с резервной копией целей
foreach (var src in Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories))
{
var rel = Path.GetRelativePath(sourceDir, src);
var dst = Path.Combine(installPath, rel);
var dstDir = Path.GetDirectoryName(dst)!;
Directory.CreateDirectory(dstDir);
if (File.Exists(dst))
{
var bkp = Path.Combine(backupDir, rel);
Directory.CreateDirectory(Path.GetDirectoryName(bkp)!);
File.Copy(dst, bkp, overwrite: true);
}
File.Copy(src, dst, overwrite: true);
}
completed = true;
_log.Info("Install completed successfully.");
}
catch (Exception ex)
{
_log.Error($"Install failed: {ex.Message}");
_log.Warn("Rolling back...");
Rollback(backupDir, installPath);
throw;
}
finally
{
// Очистка резервной копии только в случае успеха
if (completed)
{
TryDeleteDirectory(backupDir);
}
}
}
private static void Rollback(string backupDir, string installPath)
{
if (!Directory.Exists(backupDir)) return;
foreach (var bkp in Directory.EnumerateFiles(backupDir, "*", SearchOption.AllDirectories))
{
var rel = Path.GetRelativePath(backupDir, bkp);
var dst = Path.Combine(installPath, rel);
Directory.CreateDirectory(Path.GetDirectoryName(dst)!);
File.Copy(bkp, dst, overwrite: true);
}
}
private static void TryDeleteDirectory(string dir)
{
try { Directory.Delete(dir, true); } catch { /* ignore */ }
}
}

View File

@@ -0,0 +1,67 @@
namespace Updater.Core;
/// <summary>
/// Управляет потоком обновлений: извлечение, установка, перезапуск.
/// </summary>
public sealed class UpdaterApp
{
private readonly ILogger _log;
private readonly IExtractor _extractor;
private readonly IInstaller _installer;
private readonly IProcessManager _proc;
public UpdaterApp(ILogger log, IExtractor extractor, IInstaller installer, IProcessManager proc)
{
_log = log;
_extractor = extractor;
_installer = installer;
_proc = proc;
}
/// <summary>Запускает рабочий процесс обновления.</summary>
public int Run(Options opts)
{
var tempExtractDir = Path.Combine(Path.GetTempPath(), $"upd_extract_{Guid.NewGuid():N}");
Directory.CreateDirectory(tempExtractDir);
try
{
_extractor.Extract(opts.ZipPath, tempExtractDir);
}
catch (Exception ex)
{
_log.Error($"Extraction failed: {ex.Message}");
Cleanup(tempExtractDir);
return ExitCodes.ExtractFailed;
}
try
{
_installer.Install(tempExtractDir, opts.InstallPath);
}
catch (Exception ex)
{
_log.Error($"Install failed: {ex.Message}");
Cleanup(tempExtractDir);
return ExitCodes.InstallFailed;
}
Cleanup(tempExtractDir);
try
{
_proc.StartApp(opts.InstallPath, opts.AppExe, opts.RestartDelayMs);
return ExitCodes.Ok;
}
catch (Exception ex)
{
_log.Error($"Restart failed: {ex.Message}");
return ExitCodes.RestartFailed;
}
}
private static void Cleanup(string dir)
{
try { Directory.Delete(dir, true); } catch { /* ignore */ }
}
}

View File

@@ -0,0 +1,18 @@
using System.IO.Compression;
namespace Updater.Core;
/// <summary>ZIP-экстрактор с использованием System.IO.Compression.</summary>
public sealed class ZipExtractor : IExtractor
{
private readonly ILogger _log;
public ZipExtractor(ILogger log) => _log = log;
public void Extract(string archivePath, string targetDir)
{
_log.Info($"Extracting '{archivePath}' to '{targetDir}'...");
if (Directory.Exists(targetDir)) Directory.Delete(targetDir, true);
Directory.CreateDirectory(targetDir);
ZipFile.ExtractToDirectory(archivePath, targetDir, overwriteFiles: true);
}
}

29
Updater/Program.cs Normal file
View File

@@ -0,0 +1,29 @@
using Updater.Core;
namespace Updater;
internal sealed class Program
{
static int Main(string[] args)
{
var logger = new ConsoleLogger();
Options? options;
try
{
options = Options.Parse(args);
}
catch (Exception ex)
{
logger.Error($"Arguments error: {ex.Message}");
Console.WriteLine(Options.Usage);
return ExitCodes.InvalidArgs;
}
var extractor = new ZipExtractor(logger);
var installer = new SafeFileInstaller(logger);
var procMgr = new ProcessManager(logger);
var app = new UpdaterApp(logger, extractor, installer, procMgr);
return app.Run(options);
}
}

10
Updater/Updater.csproj Normal file
View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>