Как создавать и эксплуатировать службы Windows — от выбора между планировщиком заданий и службами до превращения BackgroundService в службу Windows

· · Служба Windows, Windows, .NET, C#, BackgroundService, Generic Host, Резидентная обработка, Эксплуатация, Таблица решений, Техническая консультация

«Планировщик заданий с интервалом в 5 минут уже не успевает». «Хотим держать резидентного демона, постоянно ожидающего данные от устройства». «Если в папку попадает файл, его нужно обработать в течение нескольких секунд». Стоит продолжить консультации по периодическому выполнению — и рано или поздно требования вырастают именно до этого разговора.

На этом блоге мы уже писали о смежных темах автоматизации: «Автоматизация бизнес-процессов с Power Automate» и «Надёжное и безопасное проектирование эксплуатации планировщика заданий». В 8-й главе статьи о планировщике заданий была проведена граница: как только нужен поллинг с интервалом в минуты или постоянный мониторинг, вы уже находитесь в области резидентных процессов. Но как на самом деле создать службу Windows и вывести её в эксплуатацию? Если оставить этот вопрос расплывчатым и просто «на всякий случай сделать службой», рождаются службы, которые нельзя остановить, службы, которые падают, а никто этого не замечает, и службы, работающие под LocalSystem, которым дозволено абсолютно всё.

В этой статье в порядке, в котором это решается на практике, разберём: таблицу решений — стоит ли превращать резидентную обработку в службу Windows; минимальные знания об устройстве службы (SCM, типы запуска, сессия 0); реализацию на основе Worker Service в .NET 8; регистрацию через sc.exe и параметры восстановления; выбор учётной записи выполнения; и, наконец, безопасную остановку.

1. Сначала вывод

  • Критерий выбора прост. Если это периодическая обработка с интервалом от нескольких минут — достаточно планировщика заданий. Если в требования входят постоянное ожидание (сокеты, именованные каналы, FileSystemWatcher), реакция за секунды или автоматическое восстановление после падения — нужна служба Windows.
  • Начиная с Windows Vista службы работают в сессии 0 и не могут напрямую взаимодействовать с пользователем (не могут показывать UI). Если UI нужен, разделите службу и UI-приложение и наладьте обмен через межпроцессное взаимодействие.1
  • Официальный путь разработки в .NET — шаблон Worker Service плюс AddWindowsService из пакета Microsoft.Extensions.Hosting.WindowsServices (для семейства IHostBuilderUseWindowsService). Приложение можно напрямую запускать как консольное, поэтому разработка и отладка стали значительно проще, чем при традиционной разработке служб.2
  • Начиная с .NET 6, необработанное исключение внутри BackgroundService по умолчанию останавливает хост (BackgroundServiceExceptionBehavior.StopHost). Но поскольку это считается «штатной остановкой», перезапуск через параметры восстановления SCM не срабатывает. Для сбоев, после которых нужен перезапуск, завершайте процесс с ненулевым кодом возврата.32
  • Не выбирайте LocalSystem для учётной записи выполнения просто по инерции. Поскольку sc.exe create по умолчанию использует LocalSystem, без сознательного решения служба начнёт работать с максимальными правами.4 Для чисто локальной обработки подойдёт LocalService или виртуальная учётная запись, а для доступа к общим ресурсам или БД в домене основной выбор — gMSA.5
  • Параметры восстановления (sc.exe failure) и остановку (HostOptions.ShutdownTimeout, по умолчанию 30 секунд6) нужно продумывать уже на этапе регистрации. «Служба, не реагирующая на остановку» — самое ненавидимое явление в эксплуатации.

2. Когда стоит делать службу — таблица решений: планировщик заданий, служба, резидентное приложение

Когда появляется требование, похожее на «постоянную работу», вариантов три: планировщик заданий, служба Windows и «резидентное приложение, зарегистрированное в автозагрузке» (тип, который живёт в области уведомлений). Сопоставим их особенности.

Аспект Планировщик заданий Служба Windows Резидентное приложение (регистрация в автозагрузке)
Момент запуска Запускается по времени или событию Резидентна с момента загрузки ОС (до входа пользователя) С момента входа пользователя
Нужен ли вход в систему Можно обойтись без входа (неинтерактивный запуск) Не нужен Нужен (исчезает при выходе)
UI Нельзя показать (в неинтерактивной конфигурации) Нельзя показать (сессия 0) Можно показать (область уведомлений, диалоги)
Постоянно или периодически Периодически (уместно для интервалов от часа до суток) Постоянно Постоянно (но в рамках сеанса пользователя)
Права Учётная запись выполнения задаётся для каждой задачи Учётная запись службы (глава 6) Права вошедшего пользователя
Мониторинг и восстановление История + собственные уведомления Параметры восстановления SCM (автоматический перезапуск) Отсутствует (нужно делать самостоятельно)
Затраты на развёртывание Регистрация через XML / PowerShell sc.exe / инсталлятор Регистрация в ключе Run и т. п.

Критерии выбора такие.

  • Периодическая обработка от нескольких раз в день до раза в час, без сохранения состояния между запусками — планировщик заданий. Специально превращать это в службу и вручную управлять таймером избыточно: эксплуатационное проектирование, описанное в «статье о планировщике заданий», обойдётся значительно дешевле.
  • Если суть в постоянном ожидании — ожидание запросов через TCP или каналы, отслеживание папки через FileSystemWatcher, последовательная обработка очереди, реакция за секунды — нужна служба Windows. Если вы начинаете дробить работу на «задачу каждые 5 минут» с поллингом, это признак того, что вы неудачно переизобретаете резидентный процесс.
  • Если главное — UI (управление из области уведомлений, всплывающие уведомления пользователю) — это резидентное приложение. Но оно работает только при выполненном входе, поэтому для серверных сценариев не подходит. Если «фоновая обработка нужна постоянно, но нужен и UI», разделите это на два процесса — службу и UI-приложение, как описано в следующей главе.

Есть ещё один критерий, который часто упускают из виду, — восстановление. Задание планировщика, если оно падает, просто ждёт до следующего запуска по расписанию, а для службы автоматический перезапуск берёт на себя SCM (глава 5). Требование вида «даже если упадёт ночью, к утру должно само восстановиться» само по себе уже является поводом сделать службу.

3. Минимум знаний об устройстве службы — SCM, типы запуска, сессия 0

3.1 SCM и типы запуска

За кулисами служб Windows стоит диспетчер управления службами (SCM). Он ведёт реестр служб, посредничает в запросах на запуск и остановку и выполняет действия по восстановлению при сбоях. Процесс службы должен уметь разговаривать с SCM по установленному протоколу, но эту часть берут на себя библиотеки .NET (глава 4), поэтому нам как проектировщикам остаётся решить всего четыре вещи: «тип запуска», «учётная запись выполнения», «восстановление» и «остановка».

Тип запуска можно выбрать из четырёх вариантов.4

Тип запуска Поведение Где применяется
Автоматический (auto) Запускается при загрузке ОС Базовый вариант для постоянно работающей службы
Автоматический (отложенный запуск) (delayed-auto) Запускается чуть позже других автоматических служб Первый выбор для бизнес-служб: позволяет избежать перегрузки сразу после старта и незапущенных зависимостей
Вручную (demand) Запускается только по запросу Вспомогательные службы, запускаемые другими приложениями
Отключён (disabled) Запуск невозможен Меры по выводу из эксплуатации, блокировка

Для бизнес-служб отложенный запуск стоит сделать значением по умолчанию. Сразу после загрузки ОС ни сеть, ни БД, ни другие службы ещё не готовы, поэтому при максимально быстром запуске через «Автоматический» первое подключение часто оказывается неудачным. Стабильнее двухступенчатая схема: отложенный запуск создаёт временной зазор, а сбои подключения поглощаются повторными попытками внутри ExecuteAsync, о чём пойдёт речь ниже.

Стоит знать ещё об одной вещи — тайм-ауте запуска. SCM не будет бесконечно ждать сообщения о завершении запуска службы. При превышении тайм-аута по умолчанию (ServicesPipeTimeout, 30 секунд) записываются события 7000 / 7011, и запуск считается неудачным.7 Иными словами, тяжёлую инициализацию — повторные попытки подключения к БД, построение большого кэша — нельзя выполнять внутри «процедуры запуска». Железное правило: запуск должен завершаться сразу, а тяжёлая работа выполняется в основном цикле (ExecuteAsync).

3.2 Изоляция сессии 0 — служба не может показывать UI

Начиная с Windows Vista службы работают в изолированной сессии 0 и не могут напрямую взаимодействовать с пользователем.1 Вызов MessageBox.Show или показ формы внутри службы ничего не выведет на экран вошедшего пользователя. Хуже того, это классическая причина зависания: процесс будет бесконечно ждать нажатия кнопки OK, которую никто не может нажать. При переносе старого кода в службу обязательно проверьте, не остались ли в нём диалоговые окна, задуманные как отображение ошибок.

Если UI необходим, правильное решение — разделить процессы на службу (сессия 0) и UI-приложение (сеанс пользователя) и наладить обмен через межпроцессное взаимодействие (IPC). Это конфигурация, которую рекомендует сама Microsoft: используется IPC, например именованные каналы, а UI-сторона возвращает результаты службе.1 Какой именно способ IPC выбрать, разобрано в статье-компаньоне, опубликованной в тот же день, — «Таблица решений по межпроцессному взаимодействию». Если разместить UI-приложение тоже на Generic Host, оба процесса получат единообразный подход к DI, логированию и конфигурации (см. «Зачем использовать Generic Host и BackgroundService в десктопных приложениях»).

4. Как создать службу в .NET — шаблон Worker Service и AddWindowsService

4.1 Шаблон и Program.cs

Разработка службы в .NET начинается с шаблона Worker Service. Он входит в состав SDK, поэтому dotnet new worker создаёт заготовку проекта; остаётся добавить пакет Microsoft.Extensions.Hosting.WindowsServices и вызвать AddWindowsService — и приложение готово работать как служба Windows.2 Лежащий в основе механизм Generic Host разобран в статье «Что такое .NET Generic Host».

using App.MonitorService;
using Microsoft.Extensions.Hosting;

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

// Подключаем lifetime, который взаимодействует с SCM при запуске в качестве службы Windows.
// При запуске как консольного приложения продолжает работать обычный ConsoleLifetime
builder.Services.AddWindowsService(options =>
{
    options.ServiceName = "KsMonitor";
});

builder.Services.AddSingleton<MeasurementQueue>();
builder.Services.AddHostedService<MonitorWorker>();

IHost host = builder.Build();
host.Run();

Если проект написан в старом стиле с использованием Host.CreateDefaultBuilder, вместо этого вызывается расширение IHostBuilderUseWindowsService(). Роль та же.

Есть одна ловушка, по форме та же, что и проблема «Начать в (необязательно)» из статьи о планировщике заданий. Текущий каталог при запуске в качестве службы — C:\Windows\System32. Если appsettings.json и подобные файлы читаются исходя из относительного пути, получится ситуация «в консоли работает, а служба не может прочитать настройки». Разрешайте конфигурационные файлы относительно исполняемого файла либо, как советует официальный туториал, передавайте --contentRoot в binpath при регистрации, чтобы явно указать корень контента.2

4.2 ExecuteAsync — проектирование вокруг stoppingToken и исключений

Основная логика пишется в ExecuteAsync, переопределённом от BackgroundService. Здесь по сути нужно решить только два вопроса: как реагировать на запрос остановки и как обрабатывать исключения.

namespace App.MonitorService;

public sealed class MonitorWorker(
    MeasurementQueue queue,
    ILogger<MonitorWorker> logger) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        try
        {
            while (!stoppingToken.IsCancellationRequested)
            {
                try
                {
                    // Забираем один элемент из очереди и обрабатываем его. Токен обязательно передаём и в ожидание
                    var item = await queue.DequeueAsync(stoppingToken);
                    await ProcessAsync(item, stoppingToken);
                }
                catch (Exception ex) when (ex is IOException or TimeoutException)
                {
                    // Восстанавливаемый сбой: записываем в лог и повторяем попытку с задержкой
                    logger.LogError(ex, "Обработка завершилась ошибкой. Повтор через 30 секунд.");
                    await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
                }
            }
        }
        catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
        {
            // Запрос остановки от SCM. Это штатный поток, поэтому ничего не делаем.
            // Ограничение предложением when именно stoppingToken нужно, чтобы отмена,
            // вызванная, например, тайм-аутом внутренней операции, не была ошибочно
            // принята за «штатную остановку» — иначе воркер тихо остановится,
            // а хост продолжит работать
        }
        catch (Exception ex)
        {
            // Непредвиденный сбой: при поведении StopHost по умолчанию хост останавливается
            // «штатно», и параметры восстановления SCM (автоматический перезапуск) не срабатывают.
            // Завершаемся с ненулевым кодом возврата, чтобы сообщить SCM о сбое
            logger.LogCritical(ex, "Неустранимая ошибка — служба завершает работу.");
            Environment.Exit(1);
        }
    }

    private Task ProcessAsync(Measurement item, CancellationToken token)
        => throw new NotImplementedException();
}
  • Передавайте stoppingToken во все ожидания. Task.Delay, ввод-вывод, ожидание очереди. Даже одно долгое ожидание, игнорирующее токен, становится источником проблемы «служба не реагирует на остановку» (глава 7).
  • Отделяйте восстановимые сбои от невосстановимых. Обрывы сети и временные тайм-ауты перехватывайте внутри цикла, записывайте в лог и повторяйте попытку. catch (Exception), который просто проглатывает ошибку и делает continue, только создаёт службу, тихо крутящуюся вхолостую. Подход к тому, как классифицировать сбой, разобран в «Таблице решений — завершать работу или продолжать при неожиданном исключении».
  • Знайте поведение по умолчанию для необработанного исключения. До .NET 6 исключение, вышедшее за пределы ExecuteAsync, бесследно исчезало, и служба превращалась в «зомби»: выглядела работающей, но ничего не делала. Начиная с .NET 6, по умолчанию действует BackgroundServiceExceptionBehavior.StopHost: исключение записывается в лог, после чего хост останавливается.3 Но эта остановка считается «штатной», поэтому даже при настроенных параметрах восстановления перезапуска не произойдёт. Для сбоев, которые должны вызывать восстановление, процесс нужно явно завершать аварийно через Environment.Exit(1), как в коде выше, — именно такой подход описан в официальном туториале.2

4.3 Работает как обычное консольное приложение — главная причина, почему разработка стала проще

AddWindowsService переключается на lifetime для службы только при фактическом запуске в качестве службы Windows. То есть тот же exe-файл при запуске через F5 в Visual Studio или dotnet run работает как обычное консольное приложение. Точки останова срабатывают, а нажатие Ctrl+C позволяет локально проверить весь путь — от запроса остановки до StopAsync.

Однако то, что приложение работает в консоли, не гарантирует, что оно заработает и как служба. Три вещи — различие учётных записей выполнения (глава 6), сессия 0 и текущий каталог — обязательно нужно проверить именно как службу на реальной машине. Причина ситуации «в консоли работает, а как служба — нет» практически всегда сводится именно к этим трём пунктам.

5. Регистрация и эксплуатация — sc.exe, параметры восстановления, журнал событий

5.1 Регистрация через sc.exe create

Опубликованный (dotnet publish, удобнее всего — публикация в один файл) exe-файл регистрируется в SCM через sc.exe create. Выполняется из PowerShell от имени администратора.

sc.exe create "KsMonitor" binpath= "C:\Services\KsMonitor\KsMonitor.exe" start= delayed-auto obj= "NT AUTHORITY\LocalService" displayname= "KS Служба приёма данных измерений"
sc.exe description "KsMonitor" "Резидентная служба, принимающая данные измерений и регистрирующая их в БД (ответственный: отдел информационных систем)"

Сначала о классической ловушке. После знака равенства в binpath= и start= обязателен пробел. Это особенность синтаксиса sc.exe: всё до знака равенства — имя опции, а пробел между ним и значением синтаксически обязателен (если его пропустить, команда завершится ошибкой).4 Кроме того, если опустить obj=, по умолчанию используется LocalSystem.4 Если зарегистрировать службу не задумываясь, она начнёт работать с максимальными правами, поэтому выбор из главы 6 нужно сделать уже на этапе регистрации.

Настройка description незаметно, но важна. Если оставить информацию, по которой кто-то, кто откроет services.msc через несколько лет, сможет понять, что это за служба, можно ли её останавливать и к кому обращаться, — это заметно снижает будущие затраты на расследование. Удаление выполняется после остановки службы: sc.exe delete "KsMonitor".2

Если развёртывание идёт на несколько машин или регулярно происходят обновления (замена файлов), как можно раньше переходите от ручной работы с sc.exe к инсталлятору (MSI, WiX ServiceInstall и т. п.), который берёт на себя регистрацию, обновление и удаление как единый набор действий. Ручная инструкция по регистрации рано или поздно обязательно породит машину, на которой один из шагов пропустили.

5.2 Параметры восстановления — доверьте «перезапуск при падении» SCM

Одно из главных преимуществ превращения в службу — стандартные параметры восстановления SCM. Поведение при аварийном завершении процесса можно настроить декларативно.2

sc.exe failure "KsMonitor" reset= 86400 actions= restart/60000/restart/60000/restart/300000

В этом примере: «при 1-м и 2-м сбое перезапуск через 60 секунд, начиная с 3-го — через 5 минут, счётчик сбоев сбрасывается через 24 часа». Ту же настройку можно сделать и через GUI (services.msc → свойства → вкладка «Восстановление»). Прежде чем создавать собственную «сторожевую задачу» или скрипт мониторинга, воспользуйтесь этой стандартной возможностью.

Есть два момента, на которые стоит обратить внимание. Во-первых, как уже говорилось в предыдущей главе, это срабатывает только при завершении процесса с ненулевым кодом. Штатная остановка через StopHost под восстановление не подпадает. Во-вторых, если служба гарантированно падает сразу после запуска (ошибка конфигурации, недоступность БД), возникает цикл перезапусков. Именно поэтому интервал начиная с 3-й попытки стоит делать длиннее, а заодно заранее позаботиться о том, чтобы «почему упала» сохранялось в журнале событий (следующий раздел).

5.3 Совместное использование журнала событий и файлового лога

На Windows Host.CreateApplicationBuilder автоматически добавляет провайдер логирования EventLog. И по умолчанию в журнал событий попадает только уровень Warning и выше.2 Легко запутаться и подумать «после превращения в службу все логи уровня Information исчезли», но на самом деле они не исчезли, а просто отсеиваются фильтром журнала событий по умолчанию. Если нужно записывать и эксплуатационные события вроде запуска и остановки, явно укажите уровень провайдера EventLog в appsettings.json.

{
  "Logging": {
    "EventLog": {
      "SourceName": "KsMonitor",
      "LogLevel": {
        "Default": "Warning",
        "Microsoft.Hosting.Lifetime": "Information"
      }
    }
  }
}

Есть один момент, требующий внимания. Источник событий нельзя использовать для записи, пока он не зарегистрирован заранее, а для регистрации нужны права администратора. Пропуск SourceName не спасает — по умолчанию AddWindowsService использует имя приложения как имя источника2, так что в любом случае вы стартуете с «незарегистрированного источника». Учётная запись с минимальными правами вроде LocalService не может создать источник во время выполнения, и если создание не удаётся, эксплуатация начинается без единой записи в журнале событий. Регистрацию нужно выполнять на стороне инсталлятора (или в процедуре настройки с правами администратора). Это та же мысль, что и в 6-й главе «статьи о планировщике заданий»: «CreateEventSource выносится в процедуру установки».

Ориентир для разделения такой: в журнал событий попадает только то, что должен видеть эксплуатирующий персонал (запуск, остановка, сбой, восстановление), а подробная трассировка обработки идёт в собственный файловый лог. Минимальные требования к собственному файловому логу (ротация, поведение при ошибке записи) собраны в «Минимальных требованиях к самописному логгеру», а проектирование, оставляющее улики при падении всего процесса, — в «Проектировании оставления логов и дампов при сбое». В конфигурации с автоматическим перезапуском через параметры восстановления запись, сделанная в момент падения, — это единственная зацепка, которая у вас останется.

6. Учётная запись выполнения — не выбирайте LocalSystem по инерции

Служба работает в контексте безопасности указанной учётной записи: при запуске SCM выполняет вход под этой учётной записью и присваивает процессу токен.8 Это версия для служб той же темы «от чьего имени выполняется процесс», о которой шла речь в главе 3 статьи о планировщике заданий. Перечислим варианты.

Учётная запись Права Идентичность в сети Управление паролем Где применяется
LocalSystem Чрезвычайно широкие (на уровне ОС) Учётная запись компьютера Не требуется Как правило, следует избегать. Учтите, что это значение по умолчанию для sc.exe create
LocalService Минимальные Анонимная (нет доступа к общим ресурсам) Не требуется Первый выбор для чисто локальной обработки
NetworkService Минимальные Учётная запись компьютера Не требуется Лёгкая обработка с обращением к ресурсам домена
Виртуальная учётная запись (NT SERVICE\имя-службы) Только выданные права Учётная запись компьютера Не требуется ACL можно назначать индивидуально для каждой службы. Минимальные права на локальные ресурсы
gMSA Только выданные права Сама gMSA Автоматически управляется контроллером домена Основной выбор для служб, обращающихся к общим ресурсам или БД в домене

Есть три ключевых момента при выборе.

  • Не выбирайте LocalSystem только потому, что «так работает». Уязвимость в службе напрямую превращается в захват всей машины. Действительно ли нужны права уровня администратора, можно определить прямо по разбору из статьи «Когда действительно нужны права администратора». В большинстве случаев нужна лишь, например, «запись в определённую папку», а для этого достаточно назначить ACL на папку виртуальной учётной записи (NT SERVICE\KsMonitor). Виртуальная учётная запись не требует ни создания, ни управления паролем.5
  • Ловушка сетевых общих ресурсов. LocalService в сети выступает как анонимная учётная запись, поэтому доступ к \\server\share завершится неудачей. NetworkService, виртуальные учётные записи и LocalSystem выходят в сеть как учётная запись компьютера (DOMAIN\имя-машины$)5, поэтому на стороне общего ресурса нужно выдать разрешения — как на уровне общего доступа, так и NTFS — именно учётной записи компьютера. Ситуация «пользователю разрешение выдали, а только служба не может читать» почти всегда объясняется этим. Сначала определите, «от чьего имени вы выходите в сеть», а уже потом настраивайте общий ресурс.
  • Если запускаете под обычной пользовательской учётной записью, учитывайте и эксплуатацию пароля. При использовании выделенной учётной записи нужно право «вход в систему как служба», а после истечения срока действия пароля вход не выполнится и служба не сможет запуститься.8 Это версия для служб проблемы «задача тихо умирает после смены пароля». В доменной среде правильный ответ — убрать эту проблему целиком с помощью gMSA, чей пароль автоматически управляется доменом (тот же вывод, что и в разделе 3.2 статьи о планировщике заданий).

7. Безопасная остановка — ShutdownTimeout и незавершённая обработка

Разберём последовательность остановки. Когда services.msc, sc.exe stop или завершение работы ОС заставляют SCM выдать запрос на остановку, lifetime службы .NET преобразует это в остановку хоста (StopApplication), stoppingToken отменяется, ExecuteAsync завершается, и вызывается StopAsync каждой службы. Время, в течение которого хост ждёт завершения всей этой цепочки, задаётся HostOptions.ShutdownTimeout, и по умолчанию оно равно 30 секундам.69 Если не уложиться, остановка выполняется принудительно, не дожидаясь завершения последующей обработки.

Если 30 секунд недостаточно, чтобы дописать одну незавершённую операцию, увеличьте тайм-аут, отталкиваясь от нужного времени.

builder.Services.Configure<HostOptions>(options =>
{
    // Отталкиваемся от максимального времени, необходимого для завершения незаконченной обработки
    options.ShutdownTimeout = TimeSpan.FromSeconds(90);
});

После этого проектирование остановки сводится к трём пунктам.

  • Заранее определите последовательность действий после запроса остановки. Прекратить приём новой работы → завершить незавершённую обработку (или прервать её в безопасной точке и записать в форме, допускающей возобновление) → привести в порядок соединения и временные файлы. Если этот порядок не определён, вы получаете одну из двух крайностей: бросить всё в момент получения токена или пытаться доделать всё и не уложиться во время.
  • Не создавайте «службу, не реагирующую на остановку». Достаточно одного долгого цикла или ожидания, игнорирующего токен, чтобы служба зависла в SCM в состоянии «останавливается». Это блокирует перезагрузки Windows Update и требует ручного kill при каждой плановой остановке сервера — один из самых ненавидимых недостатков в эксплуатации. Как и в главе 4, проверяйте токен на каждой границе обработки и передавайте CancellationToken в любой длительный ввод-вывод. При запуске в консоли этот поток остановки можно проверить прямо через Ctrl+C, поэтому рекомендуем включить в список тестов перед релизом пункт «через сколько секунд после Ctrl+C завершается работа».
  • Приоритизируйте конструкцию, которая не ломается даже при принудительном завершении, выше аккуратности процедуры остановки. Отключение питания или принудительное завершение случаются, как бы тщательно вы ни проектировали остановку. Транзакции, атомарная запись файлов, единицы обработки, допускающие повторный запуск, — вот основа: конструкция, при которой «даже если процесс умер посередине, при следующем запуске согласованность восстанавливается», является главным, а аккуратная остановка — всего лишь вопрос хорошего тона.

8. Итог

Решение простое. Периодическая обработка — планировщик заданий; если нужны постоянное ожидание, реакция за секунды или автоматическое восстановление — служба Windows; если главное — UI, резидентное приложение. Если решено делать службу, сегодняшний стандартный путь — Worker Service и AddWindowsService в .NET, то есть «разрабатывать как консольное приложение, поставлять как службу».

На этапе регистрации нужно спроектировать четыре вещи: тип запуска (для бизнес-служб — отложенный запуск), учётную запись выполнения (избегать LocalSystem, использовать виртуальную учётную запись или gMSA), параметры восстановления (sc.exe failure в паре с Environment.Exit(1)) и остановку (ShutdownTimeout и приведение в порядок незавершённой обработки). Если проработать эти четыре пункта, а также написать ExecuteAsync с уважением к stoppingToken, служба Windows перестаёт быть «чем-то особенно сложным для создания». И наоборот, служба, запущенная в эксплуатацию без решения этих четырёх пунктов, через несколько лет вернётся тройным проклятием: её нельзя остановить, никто не замечает, когда она падает, и у неё слишком много прав, чтобы её трогать. Восстановление существующей службы тоже быстрее всего начинать с ревизии именно этих четырёх пунктов.

Похожие статьи

Смежные области консультаций

Komura Software Co., Ltd. занимается ревью проектирования резидентной обработки (включая решение о переходе с планировщика заданий), а также консультациями по расследованию и восстановлению служб Windows, которые «не реагируют на остановку» или «незаметно падают».

Источники

  1. Microsoft Learn, Interactive Services. О том, что начиная с Windows Vista службы не могут напрямую взаимодействовать с пользователем, выполняются в сессии 0, и что при необходимости UI рекомендуется конфигурация с отдельным процессом GUI-приложения, взаимодействующим со службой через IPC.  2 3

  2. Microsoft Learn, Create Windows Service using BackgroundService. Официальный туториал по превращению Worker Service в службу Windows: об AddWindowsService, об уровне Warning по умолчанию у провайдера EventLog, о регистрации, восстановлении и удалении через sc.exe, а также о том, что параметры восстановления требуют ненулевого кода возврата.  2 3 4 5 6 7 8 9

  3. Microsoft Learn, .NET 6 breaking change: Exception handling in hosting. О том, что начиная с .NET 6 необработанное исключение в BackgroundService.ExecuteAsync записывается в лог и по умолчанию останавливает хост (BackgroundServiceExceptionBehavior.StopHost).  2

  4. Microsoft Learn, sc.exe create. О спецификации start= (auto / demand / delayed-auto и т. д.) и obj= (по умолчанию LocalSystem), а также о том, что после знака равенства опции обязателен пробел (при пропуске команда завершается ошибкой).  2 3 4

  5. Microsoft Learn, Service Accounts in Windows Server. О том, что виртуальная учётная запись (NT SERVICE\имя-службы) обращается к сети с учётными данными учётной записи компьютера без управления паролем, и о том, что пароль gMSA автоматически управляется на стороне домена.  2 3

  6. Microsoft Learn, .NET Generic Host. О последовательности завершения работы хоста и о том, что при остановке ожидание идёт в течение настраиваемого тайм-аута завершения (по умолчанию 30 секунд).  2

  7. Microsoft Learn, A service does not start, and events 7000 and 7011 are logged. О том, что SCM ждёт запуска службы только время, заданное ServicesPipeTimeout, а при превышении записывает события 7000 / 7011, а также о процедуре увеличения тайм-аута по умолчанию. 

  8. Microsoft Learn, Service User Accounts. О том, что служба выполняется в контексте безопасности указанной учётной записи, что SCM выполняет вход при запуске, что при истёкшем пароле служба не может запуститься, а также о специальных учётных записях LocalService / NetworkService / LocalSystem.  2

  9. Microsoft Learn, HostOptions.ShutdownTimeout Property. Об определении свойства, управляющего тайм-аутом по умолчанию для StopAsync

Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.

Эти страницы показывают тему статьи в более широком контексте услуг и решений.

Статья напрямую связана со следующими услугами.

Частые вопросы

Вопросы, которые часто возникают при консультациях по теме статьи.

Как разграничить использование планировщика заданий и службы Windows?
Если это периодическая обработка с интервалом от нескольких минут и без сохранения состояния между запусками, планировщика заданий достаточно. Если в требования входят постоянное ожидание через TCP или именованные каналы, отслеживание папки через FileSystemWatcher, реакция за секунды или автоматическое восстановление после падения — нужна служба Windows. Если вы начинаете дробить работу на «задачу каждые 5 минут» с поллингом, это признак того, что вы неудачно переизобретаете резидентный процесс. Если же главное — UI (например, управление из области уведомлений), появляется третий вариант: резидентное приложение, которое работает только во время сеанса пользователя.
Может ли служба Windows показывать UI (экран)?
Нет. Начиная с Windows Vista службы работают в изолированной сессии 0 и не могут напрямую взаимодействовать с пользователем. Вызов MessageBox.Show внутри службы ничего не покажет на экране пользователя и приведёт к зависанию: процесс будет бесконечно ждать нажатия кнопки OK, которую никто не может нажать. Если UI нужен, правильное решение — разделить службу и UI-приложение на отдельные процессы и наладить обмен через межпроцессное взаимодействие, например именованные каналы. При переносе старого кода в службу обязательно проверьте, не остались ли в нём диалоговые окна с сообщениями об ошибках.
Как создать службу Windows в .NET?
Официальный путь — начать с шаблона Worker Service (dotnet new worker), добавить пакет Microsoft.Extensions.Hosting.WindowsServices и вызвать AddWindowsService. Тот же exe-файл при запуске через F5 в Visual Studio или dotnet run работает как обычное консольное приложение, поэтому разработка и отладка становятся значительно проще. Регистрация выполняется через sc.exe create, но нужно помнить о характерной особенности: после знака равенства в binpath= и start= обязателен пробел, а если пропустить obj=, по умолчанию используется LocalSystem (максимальные права). Текущий каталог при запуске службы — C:\Windows\System32, поэтому конфигурационные файлы нужно разрешать относительно исполняемого файла.
Что происходит со службой при исключении внутри BackgroundService?
Начиная с .NET 6, необработанное исключение, вышедшее за пределы ExecuteAsync, по умолчанию (BackgroundServiceExceptionBehavior.StopHost) записывается в лог, после чего останавливает хост. Однако это считается «штатной остановкой», поэтому параметры восстановления SCM (автоматический перезапуск) не срабатывают. Для сбоев, после которых нужен перезапуск через параметры восстановления, процесс нужно явно завершать аварийно с ненулевым кодом возврата — например, через Environment.Exit(1). Восстанавливаемые сбои, такие как обрыв сети, следует перехватывать внутри цикла, записывать в лог и повторять попытку, отделяя их от невосстановимых сбоев ещё на этапе проектирования.

Об авторе

Страница с профилем автора статьи.

Го Комура

Представитель KomuraSoft LLC

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

Публичные ссылки

Вернуться в блог