Совсем недавно мне пришлось работать над приложением, которое собирает данные из внешних систем, обрабатывает их и отправляет в центральную базу данных. Это приложение WinForms с несколькими экранами для настройки и ведения журнала. Я искал разные подходы к созданию новой версии парсера и углублялся в варианты использования службы Windows.

Я забыл, откуда взялась точная идея, но в какой-то момент я подумал: а что, если я смогу использовать Blazor в качестве пользовательского интерфейса для службы Windows. Затем я мог настраивать, запускать и останавливать, в основном делать все что угодно с этим сервисом с графическим интерфейсом. Я также читал кое-что о службах systemd с .NET, поэтому я мог бы даже создать кросс-платформенную версию (не то, чтобы Linux был нужен, но я сам использую Linux, я бы тоже хотел попробовать).

Что ж, через несколько часов у меня все заработало, и в этом посте я покажу вам, как создать кроссплатформенную службу, которую можно установить в Windows как службу и в Linux как компонент systemd и управлять ею с помощью Blazor.

Существует много информации о том, как запустить проект .NET как службу в Windows и Linux (насколько я знаю, Mac пока не поддерживается). Я предоставлю пример проекта на GitHub и покажу здесь только некоторые основы.

Наиболее интересной частью для меня является размещение приложения Blazor Server с Kestrel в качестве службы как в Windows, так и в Linux. Это дает безграничные возможности в управлении сервисом, причем не из самой системы, а удаленно.

Управление сервисом с помощью Blazor

Сначала мы создаем обычный проект Blazor Server. Я сохраняю проект как есть и просто добавляю несколько классов, чтобы продемонстрировать использование Blazor в службе.

Добавление фоновой службы

Создайте класс с именем CustomBackgroundService. Я использую BackgroundService в качестве базового класса, но я также могу реализовать IHostedService. Более подробную информацию о различных типах можно найти здесь.

Эта служба просто регистрирует, а затем ждет 5 секунд, чтобы имитировать процесс, который выполняется некоторое время:

public class CustomBackgroundService : BackgroundService
{
    public bool IsRunning { get; set; }
    private readonly ILogger<CustomBackgroundService> _logger;
    public CustomBackgroundService(ILogger<CustomBackgroundService> logger) => _logger = logger;
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
		try
		{
		    _logger.LogInformation($"{nameof(CustomBackgroundService)} starting {nameof(ExecuteAsync)}");
		    IsRunning = true;
		    while (!stoppingToken.IsCancellationRequested)
		    {
			    _logger.LogInformation($"{nameof(CustomBackgroundService)} running {nameof(ExecuteAsync)}");
			    await Task.Delay(5000);
		    }
		    IsRunning = false;
		    _logger.LogInformation($"{nameof(CustomBackgroundService)} ending {nameof(ExecuteAsync)}");
	    }
	    catch (Exception exception)
	    {
		    _logger.LogError(exception.Message, exception);
	    }
	    finally
	    {
		    IsRunning = false;
	    }
    }
}

Регистрация и добавление размещенного сервиса:

services
	.AddLogging(logging => logging.AddConsole())
	.AddSingleton<WeatherForecastService>()
	.AddSingleton<CustomBackgroundService>()
	.AddHostedService(serviceCollection => serviceCollection.GetRequiredService<CustomBackgroundService>());

Я добавил новую страницу Razor и добавил ее в меню (Pages/Service.razor):

@page "/Service"
@inject CustomBackgroundService _customBackgroundService
<h3>Service</h3>
<p><div hidden="@HideIsRunning">Running</div></p>
<button name="startButton" class="btn btn-primary" @onclick="Start">Start</button>
<button class="btn btn-primary" @onclick="Stop">Stop</button>
@code {
    private bool _isRunning { get; set; }
    public bool HideIsRunning => !_isRunning;
    
    protected override void OnInitialized()
    {
        _isRunning = _customBackgroundService.IsRunning;
        base.OnInitialized();
    }
    private async Task Start()
    {
        if(!_customBackgroundService.IsRunning)
            await _customBackgroundService.StartAsync(new System.Threading.CancellationToken());
        _isRunning = _customBackgroundService.IsRunning;
    }
    
    private async Task Stop()
    {
        if(_customBackgroundService.IsRunning)
            await _customBackgroundService.StopAsync(new System.Threading.CancellationToken());
        _isRunning = _customBackgroundService.IsRunning;
    }
}

Добавление нового пункта меню в приложение Blazor по умолчанию путем изменения «Shared/NavMenu.razor»:

<div class="nav-item px-3">
    <NavLink class="nav-link" href="Service">
	    <span class="oi oi-pulse" aria-hidden="true"></span> Service
    </NavLink>
</div>

При отладке проекта это должно быть видно:

вы можете запускать и останавливать службу и проверять консоль вашей IDE, чтобы увидеть вывод:

Запуск и установка сервиса

Чтобы запустить приложение как службу, необходимо добавить следующий код в Program.cs:

public static void Main(string[] args)
{
    var isService = !(Debugger.IsAttached || args.Contains("--console"));
    if (isService)
	    Directory.SetCurrentDirectory(Environment.ProcessPath!);
    var builder = CreateHostBuilder(args.Where(arg => arg != "--console").ToArray());
    if (isService)
    {
	    if (OperatingSystem.IsWindows())
		    builder.UseWindowsService();
	    else if (OperatingSystem.IsLinux())
		    builder.UseSystemd();
	else
	    throw new InvalidOperationException(
		$"Can not run this application as a service on this Operating System");
    }
    builder.Build().Run();
}

Затем установите следующие пакеты NuGet:

Служба .NET в Windows

Сначала опубликуйте приложение в папке. Обязательно создайте правильный профиль публикации для Windows. Кроме того, подумайте, нужно ли вам публиковать его: Framework-Dependent (платформа .NET должна быть установлена ​​на хост-компьютере) или Self-Contained (все необходимое включено, как здорово!):

После публикации откройте командную строку Powershell, перейдите в каталог, содержащий только что опубликованный сервис, и выполните следующие команды:

  • New-Service -Name «Фоновая служба Blazor» -BinaryPath .\BlazorBackgroundservice.BlazorUI.exe

  • Start-Service -Name «BlazorBackgroundService»

Я мог бы записывать логи в регистратор событий, но решил записывать простые логи в текстовый файл. Когда вы заглянете в каталог службы, вы должны увидеть файл журнала log****.txt. Посмотрите в файл журнала, чтобы увидеть, работает ли служба. При переходе по URL-адресу, указанному в файле журнала, имейте в виду, что порт HTTPS может не работать, поскольку не установлены действительные сертификаты SSL.

Служба .NET в Linux

То же, что и для Windows: опубликуйте приложение в папке, используя правильную конфигурацию публикации. Я могу протестировать приложение, добавив --console в командную строку:

Чтобы установить его как службу, я создал файл /etc/systemd/system/blazorbackgroundservice.service:

[Unit]
Description=Blazor Background Service
[Service]
Type=Notify
ExecStart=/home/jacob/blazorbackgroundservice/linux/BlazorBackgroundService.BlazorUI
[Install]
WantedBy=multi-user.target

Выполните следующие команды:

  • sudo systemctldaemon-reload
  • статус sudo systemctlblazorbackgroundservice

  • запуск sudo systemctl blazorbackgroundservice
  • состояние sudo systemctl blazorbackgroundservice

Оно работает! Проверьте выходные данные состояния для URL-адреса веб-сайта Blazor и перейдите на сайт, чтобы проверить, работает ли он.

Вы даже можете автоматически запустить службу, выполнив следующую команду:

  • sudo systemctl включает blazorbackgroundservice

При следующей перезагрузке служба запустится автоматически.

Заключение

Хотя это короткий пост, в котором нет ничего особенного, мне самому не терпится реализовать некоторые сервисы таким образом. Я все еще думаю о некоторых вариантах использования, но я могу представить сервис, который собирает данные, обрабатывает их и отправляет в онлайн-сервис. Есть мысли о других вариантах использования? Пожалуйста, дайте мне знать!

Источники