Демоны / Автоперезапуск и reload сервисов

И так у нас сейчас есть сервис он же демон, который работает потенциально бесконечно.

Но что будет, если по какой-то причине в коде произойдет непредвиденная ситуация и произойдет исключение.

Давайте попробуем смоделировать такую ситуацию.

Генерим ошибку

Я добавлю в событие генерацию ошибки. Так как OnChanged на самом деле вызывается асинхронно то я не могу просто взять и сгенерить ошибку в нем. Типа так:

private static void OnChanged(object sender, FileSystemEventArgs e)
{
    throw new Exception("Падаю =ООО"); // добавил ошибку
    
    Console.WriteLine($"Изменился: {e.FullPath}");
}

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

class Program
{
    static bool errorRaised = false; // добавил поле, которое будет уходить в true при возникновении ошибки

    static int Main(string[] args)
    {
        // ...

        while(true) {
            Thread.Sleep(2000);
            Console.WriteLine("Мониторю");
            
            // проверяю errorRaised и если оно true, то кидаю ошибку
            if (errorRaised) {
                throw new Exception("Падаю =ООО"); 
            }
        }    
    }

    private static void OnChanged(object sender, FileSystemEventArgs e)
    {
        errorRaised = true; // тут меняю значение на true чтобы сгенерить ошибку
        Console.WriteLine($"Изменился: {e.FullPath}");
    }
}

перебилдиваем и перезапускаем

dotnet build
sudo systemctl restart my_daemon.service

запускаем журнал

journalctl -u my_daemon.service -f

и в параллельной консольке пробуем чего-нибудь вписать в файл

как видим процесс умирает с концами и перестает мониторить.

Подключаем автоперезапуск

Одной из важнейших задач супервизора является оживление упавших процессов.

В общем, чтобы наш процесс после умирания сразу восстанавливался необходимо добавить в описание сервиса (файл my_daemon.service) строчку

[Unit]
Description=Заклинатель книг

[Service]
User=m
Restart=on-failure # вот эту
WorkingDirectory=/home/m/projects/daemon/bin/Debug/net5.0
ExecStart=/home/m/projects/daemon/bin/Debug/net5.0/daemon -f config.txt

[Install]
WantedBy=multi-user.target

полный список допустимых значений такой

вообще почитать про разные параметры для сервиса можно тут

man systemd.service

после того как подправите файлик надо обновить кэш systemd и перезапустить сервис

sudo systemctl daemon-reload
sudo systemctl restart my_daemon.service

опять запускаем параллельно лог и процесс и наблюдаем как процесс перезапустился

красота! =)

Перехватываем HUP сигнал

Настраиваем .net 6

Убедитесь, что у вас установлен dotnet-sdk-6.0

sudo apt install dotnet-sdk-6.0 

проверьте что ваш проект использует .net 6.0, откройте файлик проекта

nano daemon.csproj

и проверьте что там что-то в этом роде, нас интересует пункт TargetFramework

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
  </PropertyGroup>

</Project>

если там написано net5.0 то просто поправьте на net6.0 и пересоберите

так как демон соберется в папку bin/Debug/net6.0/ надо подправить файл сервиса, надо обновить пути к программе

WorkingDirectory=/home/m/projects/daemon/bin/Debug/net6.0
ExecStart=/home/m/projects/daemon/bin/Debug/net6.0/daemon -f config.txt

Команда reload

В systemd помимо уже знакомых нам команд start, restart, stop встроена еще одна полезная команда, которая называется reload.

Это такой облегченный вариант restart, который предполагает, что сервис не будет останавливаться, а просто обновит свою внутреннюю конфигурацию, например перечитает файл конфига.

Правда если попробовать запустить эту команду, то увидим что-то такое

ну то есть наш сервис не поддерживает reload

Чтобы он стал его поддерживать надо добавить пункт ExecReload в описание сервиса

[Unit]
Description=Заклинатель книг

[Service]
User=m
Restart=on-failure
WorkingDirectory=/home/m/projects/daemon/bin/Debug/net5.0
ExecStart=/home/m/projects/daemon/bin/Debug/net5.0/daemon -f config.txt
ExecReload=/bin/kill -HUP $MAINPID # вот тут

[Install]
WantedBy=multi-user.target

теперь если попробовать выполнить reload сервиса мы увидим, что процесс останавливается.

Дело в том, что по умолчанию HUP сигнал обрабатывается как сигнал остановки. Причем он считается корректным сигналом остановки и супервизор не пытается перезапустить процесс.

В общем, чтобы поменять такое поведение надо подключить собственный обработчик к этому сигналу.

В .net 6 как раз добавили поддержку linux сигналов и можно легко сделать собственный обработчик, если запихать в Main что-то такое

using System;
using System.IO;
// ...
using System.Runtime.InteropServices;  // не забывем добавить using

namespace daemon
{
    class Program
    {
        static int Main(string[] args)
        {
            // подключаем обработчик
            PosixSignalRegistration.Create(PosixSignal.SIGHUP, (context) => {
                context.Cancel = true; // отключаем обработчик по умолчанию
                Console.WriteLine("обновляюсь без перезагрузки"); 
            });
            // ...
        }
        
        // ...
    }
}

билдимся и запускаем процесс по новой

sudo systemctl start my_daemon.service

вот теперь другое дело. Можно и задание пилить =)

4.3

Реализовать обработку какой-нибудь фразы в книге в результате которой программа будет падать с ошибкой.

[Необязательно] Реализовать обновление отслеживаемого файла через команду reload. То есть вы должны иметь возможность прописать путь к новому файлу в файл с конфигом, вызвать reload и убедится, что демон действительно стал мониторить новый файл.