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

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

116
README.MD Normal file
View File

@@ -0,0 +1,116 @@
# Updater
## 📖 Описание
`Updater` — это инструмент для безопасного обновления приложений.
Он состоит из двух частей:
- **ReleaseUpdater.dll** — библиотека для работы с API Gitea/GitHub:
- Получение списка доступных версий.
- Выбор нужной версии.
- Скачивание архива релиза.
- События жизненного цикла (`BeforeInstall`, `AfterInstall`, `UpdateFailed`).
- Запуск обновления либо напрямую (`UpdateInlineAsync`), либо через внешний процесс (`UpdateWithExternalAsync`).
- **Updater.exe** — консольное приложение:
- Принимает параметры (`--zip`, `--installPath`, `--appExe`).
- Распаковывает архив в папку установки.
- Перезапускает приложение.
- Работает как «чистый установщик», чтобы избежать проблем с блокировкой файлов.
---
## 🏗 Архитектура
```
Solution/
├─ ReleaseUpdater/ # Class Library (.dll)
│ ├─ ReleaseUpdaterFacade.cs
│ ├─ GiteaReleaseProvider.cs
│ ├─ HttpAssetDownloader.cs
│ ├─ SemVerService.cs
│ └─ Models.cs
├─ Updater/ # Console App (.exe)
│ ├─ Program.cs
│ ├─ Core/
│ │ ├─ IExtractor.cs / ZipExtractor.cs
│ │ ├─ IInstaller.cs / SafeFileInstaller.cs
│ │ ├─ IProcessManager.cs / ProcessManager.cs
│ │ ├─ UpdaterApp.cs
│ │ └─ Options.cs
└─ UpdaterSolution.sln
```
---
## ⚡ Возможности
- Получение списка версий из Gitea API.
- Обновление до последней или конкретной версии.
- Два режима обновления:
- **Inline** — всё внутри DLL.
- **External** — через Updater.exe.
- События:
- `BeforeInstall` — перед установкой.
- `AfterInstall` — после успешной установки.
- `UpdateFailed` — при ошибке (автоматический запуск текущей версии).
- Безопасная установка: бэкап и откат при ошибке.
---
## 🚀 Использование
### В Telegramботе
```csharp
// Получение списка версий
var versions = await ReleaseUpdaterFacade.GetVersionsAsync(apiUrl, token);
// Обновление до последней версии
await ReleaseUpdaterFacade.UpdateInlineAsync(apiUrl, token, installPath, "MyBot.exe", "latest");
// Обновление через внешний Updater.exe
await ReleaseUpdaterFacade.UpdateWithExternalAsync(apiUrl, token, installPath, "MyBot.exe", "3.5.2");
```
### Запуск Updater.exe напрямую
```bash
Updater.exe --zip "C:\Temp\update.zip" --installPath "C:\Apps\MyBot" --appExe "MyBot.exe"
```
---
## 🔧 Параметры Updater.exe
- `--zip <path>` — путь к архиву .zip.
- `--installPath <dir>` — папка установки.
- `--appExe <file.exe>` — исполняемый файл приложения.
- `--restartDelayMs <int>` — задержка перед перезапуском (по умолчанию 500 мс).
---
## 📦 Коды возврата
- `0` — успешное обновление.
- `2` — ошибка аргументов.
- `3` — ошибка распаковки.
- `4` — ошибка установки.
- `5` — ошибка перезапуска.
---
## 🛠 Пример сценария
1. Бот получает команду `/update latest`.
2. ReleaseUpdater.dll:
- Запрашивает список релизов через Gitea API.
- Скачивает архив последней версии.
- Вызывает событие `BeforeInstall`.
- Запускает Updater.exe с параметрами.
3. Updater.exe:
- Распаковывает архив.
- Перезапускает приложение.
- Возвращает код `0`.
4. ReleaseUpdater.dll вызывает событие `AfterInstall`.

7
ReleaseUpdater.slnx Normal file
View File

@@ -0,0 +1,7 @@
<Solution>
<Folder Name="/Элементы решения/">
<File Path="README.MD" />
</Folder>
<Project Path="ReleaseUpdater/ReleaseUpdater.csproj" />
<Project Path="Updater/Updater.csproj" />
</Solution>

View File

@@ -0,0 +1,85 @@
using System.Net.Http.Headers;
using System.Text.Json;
namespace ReleaseUpdater;
/// <summary>
/// Провайдер для получения информации о релизах из Github / Gitea API.
/// </summary>
public sealed class GiteaReleaseProvider
{
private static readonly JsonSerializerOptions JsonOpts = new(JsonSerializerDefaults.Web);
/// <summary>
/// Получает список всех релизов из указанного API.
/// </summary>
/// <param name="apiUrl">URL API Gitea для релизов.</param>
/// <param name="token">Токен авторизации (если требуется).</param>
/// <returns>Список релизов.</returns>
public async Task<IReadOnlyList<ReleaseInfo>> GetReleasesAsync(string apiUrl, string? token = null)
{
using var client = CreateClient(token);
using var resp = await client.GetAsync(apiUrl);
resp.EnsureSuccessStatusCode();
var stream = await resp.Content.ReadAsStreamAsync();
var releases = await JsonSerializer.DeserializeAsync<List<GiteaReleaseDto>>(stream, JsonOpts)
?? new List<GiteaReleaseDto>();
return releases.Select(Map).ToList();
}
/// <summary>
/// Находит релиз по версии или возвращает последний релиз.
/// </summary>
/// <param name="apiUrl">URL API Gitea для релизов.</param>
/// <param name="versionOrLatest">Версия (например, "3.5.2") или "latest".</param>
/// <param name="token">Токен авторизации (если требуется).</param>
/// <returns>Информация о релизе или null.</returns>
public async Task<ReleaseInfo?> FindReleaseAsync(string apiUrl, string? versionOrLatest, string? token = null)
{
var all = await GetReleasesAsync(apiUrl, token);
if (string.IsNullOrWhiteSpace(versionOrLatest) || versionOrLatest.Equals("latest", StringComparison.OrdinalIgnoreCase))
return all.FirstOrDefault();
var tag = versionOrLatest.StartsWith('v') ? versionOrLatest : $"v{versionOrLatest}";
return all.FirstOrDefault(r => string.Equals(r.TagName, tag, StringComparison.OrdinalIgnoreCase));
}
private static HttpClient CreateClient(string? token)
{
var client = new HttpClient();
client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("ReleaseUpdater", "1.0"));
if (!string.IsNullOrWhiteSpace(token))
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("token", token);
return client;
}
private static ReleaseInfo Map(GiteaReleaseDto dto) =>
new()
{
TagName = dto.tag_name,
Name = dto.name,
Assets = dto.assets?.Select(a => new AssetInfo
{
Name = a.name,
DownloadUrl = new Uri(a.browser_download_url)
}).ToList() ?? new List<AssetInfo>()
};
// DTOs для десериализации JSON
private sealed class GiteaReleaseDto
{
public string tag_name { get; set; } = "";
public string? name { get; set; }
public List<GiteaAssetDto>? assets { get; set; }
}
private sealed class GiteaAssetDto
{
public string name { get; set; } = "";
public string browser_download_url { get; set; } = "";
}
}

View File

@@ -0,0 +1,17 @@
namespace ReleaseUpdater;
public sealed class HttpAssetDownloader
{
public async Task<string> DownloadAssetAsync(Uri downloadUrl, string? token, string? downloadToFilePath = null)
{
var target = downloadToFilePath ?? Path.Combine(Path.GetTempPath(), $"updater_{Guid.NewGuid():N}.zip");
using var client = new HttpClient();
client.DefaultRequestHeaders.UserAgent.ParseAdd("ReleaseUpdater/1.0");
if (!string.IsNullOrWhiteSpace(token))
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("token", token);
var bytes = await client.GetByteArrayAsync(downloadUrl);
await File.WriteAllBytesAsync(target, bytes);
return target;
}
}

View File

@@ -0,0 +1,17 @@
namespace ReleaseUpdater;
/// <summary>
/// Информация о прикрепленных файлах
/// </summary>
public sealed class AssetInfo
{
/// <summary>
/// Название файла
/// </summary>
public required string Name { get; init; }
/// <summary>
/// Ссылка для скачивания
/// </summary>
public required Uri DownloadUrl { get; init; }
}

View File

@@ -0,0 +1,22 @@
namespace ReleaseUpdater;
/// <summary>
/// Информиция о резиле
/// </summary>
public sealed class ReleaseInfo
{
/// <summary>
/// Тэг релиза
/// </summary>
public required string TagName { get; init; }
/// <summary>
/// Заголовок релиза
/// </summary>
public string? Name { get; init; }
/// <summary>
/// Прикрепленные файлы
/// </summary>
public IReadOnlyList<AssetInfo> Assets { get; init; } = Array.Empty<AssetInfo>();
}

View File

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

View File

@@ -0,0 +1,132 @@
using System.Diagnostics;
using System.IO.Compression;
namespace ReleaseUpdater;
/// <summary>
/// Фасад для работы с релизами и обновлением.
/// Содержит события жизненного цикла.
/// </summary>
public static class ReleaseUpdaterFacade
{
/// <summary>
/// Событие вызывается перед установкой новой версии (после скачивания архива).
/// </summary>
public static event Action? BeforeInstall;
/// <summary>
/// Событие вызывается после успешной установки новой версии.
/// </summary>
public static event Action? AfterInstall;
/// <summary>
/// Событие вызывается при ошибке обновления.
/// </summary>
public static event Action<Exception>? UpdateFailed;
/// <summary>
/// Получает список доступных версий из Gitea.
/// </summary>
public static async Task<IReadOnlyList<string>> GetVersionsAsync(string apiUrl, string? token = null)
{
var provider = new GiteaReleaseProvider();
var releases = await provider.GetReleasesAsync(apiUrl, token);
return releases.Select(r => r.TagName).ToList();
}
/// <summary>
/// Обновление без Updater.exe: скачивание, распаковка и перезапуск прямо из DLL.
/// </summary>
public static async Task UpdateInlineAsync(
string apiUrl, string? token, string installPath, string appExe, string versionOrLatest = "latest")
{
try
{
var provider = new GiteaReleaseProvider();
var release = await provider.FindReleaseAsync(apiUrl, versionOrLatest, token)
?? throw new Exception("Release not found");
var asset = release.Assets.FirstOrDefault(a => a.Name.EndsWith(".zip"))
?? throw new Exception("No zip asset found");
var downloader = new HttpAssetDownloader();
var zipPath = await downloader.DownloadAssetAsync(asset.DownloadUrl, token);
BeforeInstall?.Invoke();
ZipFile.ExtractToDirectory(zipPath, installPath, true);
AfterInstall?.Invoke();
Process.Start(Path.Combine(installPath, appExe));
Environment.Exit(0);
}
catch (Exception ex)
{
UpdateFailed?.Invoke(ex);
RestartCurrent(installPath, appExe);
}
}
/// <summary>
/// Обновление через внешний Updater.exe.
/// </summary>
public static async Task UpdateWithExternalAsync(
string apiUrl, string? token, string installPath, string appExe, string versionOrLatest = "latest")
{
try
{
var provider = new GiteaReleaseProvider();
var release = await provider.FindReleaseAsync(apiUrl, versionOrLatest, token)
?? throw new Exception("Release not found");
var asset = release.Assets.FirstOrDefault(a => a.Name.EndsWith(".zip"))
?? throw new Exception("No zip asset found");
var downloader = new HttpAssetDownloader();
var zipPath = await downloader.DownloadAssetAsync(asset.DownloadUrl, token);
BeforeInstall?.Invoke();
var updaterExe = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Updater.exe");
var args = $"--zip \"{zipPath}\" --installPath \"{installPath}\" --appExe \"{appExe}\"";
var process = Process.Start(new ProcessStartInfo
{
FileName = updaterExe,
Arguments = args,
UseShellExecute = true,
WorkingDirectory = AppDomain.CurrentDomain.BaseDirectory
});
process?.WaitForExit();
if (process?.ExitCode == 0)
AfterInstall?.Invoke();
else
throw new Exception($"Updater.exe завершился с кодом {process?.ExitCode}");
Environment.Exit(0);
}
catch (Exception ex)
{
UpdateFailed?.Invoke(ex);
RestartCurrent(installPath, appExe);
}
}
private static void RestartCurrent(string installPath, string appExe)
{
var currentApp = Path.Combine(installPath, appExe);
if (File.Exists(currentApp))
{
Process.Start(new ProcessStartInfo
{
FileName = currentApp,
UseShellExecute = true,
WorkingDirectory = installPath
});
}
}
}

View File

@@ -0,0 +1,43 @@
namespace ReleaseUpdater;
/// <summary>
/// Сервис для работы с семантическими версиями.
/// </summary>
public sealed class SemVerService
{
/// <summary>
/// Пытается распарсить строку версии в объект <see cref="Version"/>.
/// </summary>
/// <param name="version">Строка версии (например, "3.5.2").</param>
/// <param name="parsed">Результат парсинга.</param>
/// <returns>true, если парсинг успешен.</returns>
public bool TryParse(string version, out Version parsed)
{
var v = version.Trim().TrimStart('v');
return Version.TryParse(Normalize(v), out parsed);
static string Normalize(string v)
{
var parts = v.Split('.', StringSplitOptions.RemoveEmptyEntries);
return parts.Length switch
{
1 => $"{parts[0]}.0.0",
2 => $"{parts[0]}.{parts[1]}.0",
_ => $"{parts[0]}.{parts[1]}.{parts[2]}"
};
}
}
/// <summary>
/// Сравнивает две версии.
/// </summary>
/// <param name="v1">Первая версия.</param>
/// <param name="v2">Вторая версия.</param>
/// <returns>-1 если v1 < v2, 0 если равны, 1 если v1 > v2.</returns>
public int Compare(string v1, string v2)
{
TryParse(v1, out var a);
TryParse(v2, out var b);
return a.CompareTo(b);
}
}

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>