Добавьте файлы проекта.
This commit is contained in:
8
Updater/Core/ConsoleLogger.cs
Normal file
8
Updater/Core/ConsoleLogger.cs
Normal 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);
|
||||
}
|
||||
10
Updater/Core/IExtractor.cs
Normal file
10
Updater/Core/IExtractor.cs
Normal 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);
|
||||
}
|
||||
12
Updater/Core/IInstaller.cs
Normal file
12
Updater/Core/IInstaller.cs
Normal 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
12
Updater/Core/ILogger.cs
Normal 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);
|
||||
}
|
||||
8
Updater/Core/IProcessManager.cs
Normal file
8
Updater/Core/IProcessManager.cs
Normal 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
66
Updater/Core/Options.cs
Normal 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;
|
||||
}
|
||||
30
Updater/Core/ProcessManager.cs
Normal file
30
Updater/Core/ProcessManager.cs
Normal 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
|
||||
});
|
||||
}
|
||||
}
|
||||
75
Updater/Core/SafeFileInstaller.cs
Normal file
75
Updater/Core/SafeFileInstaller.cs
Normal 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 */ }
|
||||
}
|
||||
}
|
||||
67
Updater/Core/UpdaterApp.cs
Normal file
67
Updater/Core/UpdaterApp.cs
Normal 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 */ }
|
||||
}
|
||||
}
|
||||
18
Updater/Core/ZipExtractor.cs
Normal file
18
Updater/Core/ZipExtractor.cs
Normal 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
29
Updater/Program.cs
Normal 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
10
Updater/Updater.csproj
Normal 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>
|
||||
Reference in New Issue
Block a user