Как написать службу для windows

В данной статье описан пошаговый процесс разработки служб для операционной системы Windows с использованием языка программирования C++.

В статье будет представлена теоретическая база по работе служб в Windows, рассмотрено их общее устройство и будет реализовано приложение, с помощью которого можно будет устанавливать свою службу, запускать её, останавливать и удалять (деинсталлировать).

Содержание

  1. Введение

  2. Что такое служба?

  3. Время существования службы

  4. Подготовительный этап

  5. С чего начинается разработка службы?

  6. Реализация функции установки службы

  7. Реализация функции запуска службы

  8. Реализация функции остановки службы

  9. Реализация функции деинсталляции службы

  10. Заключение

  11. Список рекомендованной литературы

Введение

Не так давно я начал большую часть своего рабочего времени посвящать программированию на языке C++ и в ходе решения практических задач возникла потребность написать свою службу для операционной системы Windows.

В результате поиска справочных материалов, примеров реализации служб и литературы для полноценного понимания их разработки я столкнулся с проблемой разрозненности информации в разных источниках. Узнать как полноценно разработать службу на Windows используя только один источник — сложная задача. Даже в официальной документации Майкрософт все примеры разрознены и разбросаны по разным страницам и чтобы снабдить своё приложение основными функциями приходится искать и соединять разные блоки кода из разных примеров. Кроме того исходный код примеров не лишён недостатков. Например, используя код из официальной документации можно столкнуться с проблемой отсутствия обратной связи в работе службы, потому что примеры не содержат средства для вывода сообщений о ходе работы службы, из-за чего изучение данной темы может вызвать трудности у тех, кто с этой темой только начинает работу.

Для упрощения разработки служб я подготовил начальный проект с уже встроенным простым механизмом логирования сообщений в файл, который может работать со множеством потоков и помогает получать информацию о ходе работы службы в фоновом режиме. В исходном коде приведены примеры использования данного логгера.

В рамках статьи я буду пошагово объяснять как разработать службу Windows на C++ и в результате проделанной работы получится приложение, которое умеет устанавливать службу в базу данных диспетчера управления службами, запускать эту службу, останавливать и деинсталлировать её. В общем, будет дано руководство, по которому можно разработать основные функции управления службой.

Ссылка на исходный код

Исходный код начального и конечного проекта находятся в репозитории dev-win-service.

Что такое служба?

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

Служба Windows (Windows Service) — это приложение, которое работает в операционной системе Windows в фоновом режиме, независимо от действий пользователя (если они не направлены на остановку службы).

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

Важно заметить, что сами по себе службы как приложения не имеют пользовательского интерфейса, а потому чтобы увидеть запущенную службу необходимо обратиться к диспетчеру задач и вкладке «Службы». Именно в данной вкладке можно отслеживать текущее состояние службы.

Рисунок 1 - Запущенные службы из Диспетчера задач

Рисунок 1 — Запущенные службы из Диспетчера задач

В диспетчере задач есть множество столбцов, каждый из которых даёт информацию о службе. На рисунке 1 этих столбцов 4: имя, ИД (идентификатор) процесса, описание и состояние (если сделать скролл вправо будет ещё один столбец — группа).

Идентификатор процесса службы определяет основной процесс приложения в операционной системе, которое было запущено как служба. Чтобы посмотреть подробное описание процесса приложения, запущенного как служба, можно выполнить следующую команду в программной оболочке PowerShell:

Get-Process -Id <PID>
Рисунок 2 - Пример вывода информации о процессе службы

Рисунок 2 — Пример вывода информации о процессе службы

Можно также посмотреть информацию о идентификаторе процесса службы, методе запуска, состоянии, статусе и возвращаемом коде через PowerShell скрипт по имени службы:

$service = Get-WmiObject -Class Win32_Service -Filter "Name='<NAME SERVICE>'"
$service
Рисунок 3 - Пример получения информации о службе по её имени

Рисунок 3 — Пример получения информации о службе по её имени

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

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

Рисунок 4 - Специальное окно "Службы"

Рисунок 4 — Специальное окно «Службы»

В этом окне можно выбрать отдельные службы, получить более детальную о них информацию и посмотреть полное описание служб (ведь в диспетчере задач находится только краткое описание).

Время существования службы

Служба преодолевает множество внутренних состояний за время своего существования.

С самого начала служба должна быть установлена в системе, в которой она будет выполняться. Это необходимо для того, чтобы загрузить её в диспетчер управления службами (Service Control Manager — SCM), для дальнейшей с ней работы.

SCM — это основное средство для управления службами в Windows, которое запускается при загрузке операционной системы и работает со специальной базой данных служб.

Установка службы (как и её удаление) происходит с помощью сценариев *.INF и SetupAPI. После установки службы она загружается в базу данных служб и через SCM с ней можно будет полноценно работать. Запустить службу можно сразу же после её установки, а уже запущенную службу можно остановить перед её удалением (или деинсталляцией).

Теперь разберём основные состояния службы:

  1. SERVICE_STOPPED — служба остановлена;

  2. SERVICE_RUNNING — служба выполняется;

  3. SERVICE_PAUSED — служба приостановлена (на паузе);

  4. SERVICE_STOP_PENDING — служба находится в процессе остановки;

  5. SERVICE_START_PENDING — служба находится в процессе запуска;

  6. SERVICE_PAUSED_PENDING — служба находится в процессе приостановки (постановки на паузу);

  7. SERVICE_CONTINUE_PENDING — служба находится в процессе возобновления после приостановки.

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

Подготовительный этап

Прежде чем приступить к практической части статьи и успешно воспроизвести все примеры необходимо скачать исходники программного кода с минимальным набором функций, которые потребуются на начальном этапе, а именно: готовой конфигурацией CMake и логгером, с помощью которого можно добавлять записи в файл.

Логгер особенно необходим, поскольку как мы ранее выяснили службы не имеют пользовательского интерфейса, однако они имеют доступ к файловой системе, а значит можно мониторить их работу с помощью записи в файл. Рассматривать особенности разработки логгера в данной статье мы не будем, но важно отметить, что наш логгер умеет контролировать доступ к одному файлу несколькими потоками через мьютекс, а потому мы можем быть спокойны при его использовании в разных потоках.

Загрузить начальный проект можно по этой ссылке.

После загрузки начального проекта и его запуска (необходимо убедится, что конфигурация CMake удовлетворяет вашим текущим параметрам системы) результат выполнения программы будет следующий:

Рисунок 5 - Результат выполнения программы

Рисунок 5 — Результат выполнения программы

По умолчанию в CMake значение флага DEBUG установлено в 1, чтобы выводить сообщения логгера прямо в консоль.

Рисунок 6 - Вот то место, где определён вывод в консоль если флаг DEBUG установлен в 1

Рисунок 6 — Вот то место, где определён вывод в консоль если флаг DEBUG установлен в 1

Это можно исправить изменив соответствующее значение на 0 в параметрах конфигурации CMake.

Рисунок 7 - Исправлять нужно здесь (если это необходимо)

Рисунок 7 — Исправлять нужно здесь (если это необходимо)

После выполнения кода в каталоге проекта должна быть создана директория, в которую будут складироваться логи за текущую дату (в формате YYYY-MM-DD). В моём случае это каталог 20241108. Содержимое файла лога можно увидеть ниже:

Рисунок 8 - Содержимое файла лога

Рисунок 8 — Содержимое файла лога

Убедившись, что всё работает, можно приступать к следующему шагу — разработке службы Windows.

С чего начинается разработка службы?

Для начала необходимо иметь ввиду, что разработка под платформу Windows имеет свои особенности, в том числе стандартные средства и библиотеки, которые будут использоваться.

Рекомендую ознакомится с типами данных Windows (BaseTsd.h). Из этого множества типов Windows мы будем использовать немногие, однако важно понимать что из себя представляют типы данных DWORD, LPTSTR и HANDLE.

Для начала создадим новый каталог service в корневой директории проекта dev-win-sc-base и добавим туда новые файлы: WinService.h и WinService.cpp.

Данные файлы будут определять полезные функции, которыми в будущем обрастёт наша служба. А пока что содержимое файла WinService.h должно быть следующим:

#pragma once

#include <windows.h>
#include <tchar.h>
#include <strsafe.h>
#include "../logger/logger.h"

#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "kernel32.lib")

В данном программном коде мы указываем директиву препроцессора #pragma once, которая здесь нужна для предотвращения многократного включения одного и того же заголовочного файла. Данная директива является альтернативой методу #ifndef … #define … #endif который широко распространён в большинстве проектов на C и C++, вот его пример:

#ifndef WIN_SERVICE_H
#define WIN_SERVICE_H

// Какие-то ещё директивы, определения методов и классов ...

#endif /* WIN_SERVICE_H */

Можно использовать любой из этих подходов, однако в рамках данной статьи я остановлюсь на директиве #pragma once.

Далее идёт подключение заголовочных файлов, которые необходимы для работы с компонентами Windows:

#include <windows.h>
#include <tchar.h>
#include <strsafe.h>

И указания компоновщику о добавлении необходимых библиотек в список зависимостей:

#pragma comment(lib, "advapi32.lib")
#pragma comment(lib, "kernel32.lib")

Содержимое же файла WinService.cpp должно быть следующим (довольно скромным):

#include "WinService.h"

Теперь можно определить точку доступа в службу (файл dev-win-sc.cpp) с импортированием заголовочного файла WinService.h:

#include "dev-win-sc.h"
#include "service/WinService.h"

// Точка входа в службу
int __cdecl _tmain(int argc, TCHAR *argv[])
{
    std::cout << "Hello" << std::endl;

    return 0;
}

Данный программный код также не останется без пояснений.

__cdecl — это специальное соглашение о вызовах по умолчанию для программ C и C++.

Основные особенности данного соглашения:

  1. Аргументы функций передаются через стек, справа налево;

  2. Аргументы, размер которых меньше 4 байт, расширяются до 4 байт;

  3. За сохранение регистров EAX, ECX, EDX и стека сопроцессора отвечает вызывающая программа, за остальные — вызываемая функция;

  4. Очистку стека производит вызывающая программа.

О функции _tmain лучше почитать из официальной документации Майкрософт.

Что ж, начало положено, теперь можно приступать к постепенному усложнению нашей программы.

Реализация функции установки службы

Как было уже ранее сказано в начале служба проходит этап установки и загружается в базу данных служб, с последующей возможностью её запуска через менеджер служб (SCM). С реализации данной функции и стоит начать разработку службы.

Для начала добавим в заголовочный файл WinService.h следующие макросы и объявление функции установки службы:

// Имя службы
#define SVCNAME TEXT("DevWinSc")
// Краткое описание службы
#define SVCDISPLAY TEXT("Разрабатываемая служба DevWinSc")
// Полное описание службы
#define SVCDESCRIPTION TEXT("Данная служба разрабатыватся для туториала на Habr.ru")

// Преобразование const char* в char*
#define WITHOUT_CONST(x) const_cast<char*>(x)

/* Функция установки службы */
VOID SvcInstall(void);

Как видно из комментариев в первых трёх макросах содержится имя службы, краткое описание службы и её полное описание. Дальше идёт макрос для преобразования типов и объявление функции, в которой будет описана логика установки службы.

Теперь необходимо определить в файле WinService.cpp функцию SvcInstall. Подробнее опишу этапы создания данной функции.

Для начала добавим тело функции:

VOID SvcInstall() 
{
  // ...
}

Следующим шагом будет добавление дескриптора SCM, службы, переменной для хранения пути к исполняемому файлу, структуры для полного описания службы и преобразования полного описания службы в char*:

// Дескриптор на менеджер управления службами
SC_HANDLE schSCManager;
// Дескриптор на конкретную службу (которую мы установим)
SC_HANDLE schService;
// Путь к исполняемому файлу службы (dev-win-sc.exe, который мы сначала запускаем как консольное приложение)
TCHAR szUnquotedPath[MAX_PATH];

// Структура полного описания службы
SERVICE_DESCRIPTION sd;

// Преобразование полного описания службы в LPTSTR
LPTSTR szDesc = WITHOUT_CONST(SVCDESCRIPTION);

Затем определяем путь до исполняемого файла программы, которая была запущена и в которой был произведён вызов функции SvcInstall:

// Определение пути до текущего исполняемого файла (dev-win-sc.exe)
if (!GetModuleFileName(NULL, szUnquotedPath, MAX_PATH))
{
    logger << (LogMsg() << "Не удалось установить службу (" << GetLastError() << ")");
    return;
}

// В случае, если путь содержит пробел, его необходимо заключить в кавычки, чтобы
// он был правильно интерпретирован. Например,
// "d:\my share\myservice.exe" следует указывать как
// ""d:\my share\myservice.exe""
TCHAR szPath[MAX_PATH];
StringCbPrintf(szPath, MAX_PATH, TEXT("\"%s\""), szUnquotedPath);

logger << (LogMsg() << "Путь к исполняемому файлу службы: " << szPath);

Зачем нам путь до исполняемого файла? Данный путь будет записан в базу данных служб и именно благодаря ему наша служба сможет быть запущена через отдельную функцию нашей программы (главное, чтобы исполняемый файл случайно не исчез…) в другом процессе.

Теперь необходимо получить дескриптор SCM:

// Получение дескриптора менеджера управления службами
schSCManager = OpenSCManager(
    NULL,                   // Имя компьютера
    NULL,                   // Имя конкретной базы данных служб
    SC_MANAGER_ALL_ACCESS); // Права доступа (указываем все права)

if (schSCManager == NULL)
{
    logger << (LogMsg() << "OpenSCManager вернул NULL (" << GetLastError() << ")");
    return;
}

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

Теперь, когда у нас есть дескриптор SCM можно создать (или установить) службу с последующей её загрузкой в базу данных служб и в случае, если всё прошло успешно, сразу добавим загруженной в базе данных службе новое полное описание (чтобы можно было её видеть в окне «Службы»):

// Создание службы и получение её дескриптора
schService = CreateServiceA(
    schSCManager,              // Дескриптор SCM
    SVCNAME,                   // Имя службы (отображается в диспетчере устройств)
    SVCDISPLAY,                // Краткое описание службы (отображается в диспетчере устройств и окне "Службы")
    SERVICE_ALL_ACCESS,        // Определение прав для службы (полный доступ)
    SERVICE_WIN32_OWN_PROCESS, // Тип службы
    SERVICE_DEMAND_START,      // Тип запуска
    SERVICE_ERROR_NORMAL,      // Тип контроля ошибки
    szPath,                    // Путь до исполняемого файла службы
    NULL,                      // Группа
    NULL,                      // Тег идентификатора
    NULL,                      // Зависимости
    NULL,                      // Стартовое имя (LocalSystem)
    NULL);                     // Пароль

if (schService == NULL)
{
    logger << (LogMsg() << "CreateService вернул NULL (" << GetLastError() << ")");

    // Закрытие дескриптора SCM
    CloseServiceHandle(schSCManager);
    return;
}
else
{
    logger << "Служба успешно установлена";

    // Добавляем полное описание службы
    sd.lpDescription = szDesc;

    // Инициируем операцию изменения полного описания службы, которое сейчас есть в базе данных служб
    if (!ChangeServiceConfig2(
            schService,
            SERVICE_CONFIG_DESCRIPTION,
            &sd))
    {
        logger << "ChancheServiceConfig2 вернул NULL";
    }
    else
    {
        logger << "Полное описание службы успешно установлено";
    }
}

В конце всей работы необходимо закрыть все удерживаемые дескрипторы в функции SvcInstall:

// Закрытие дескриптора службы и SCM
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);

Полный код определения функции SvcInstall

/* Установка службы и её загрузка в базу данных служб */
VOID SvcInstall()
{
    // Дескриптор на менеджер управления службами
    SC_HANDLE schSCManager;
    // Дескриптор на конкретную службу (которую мы установим)
    SC_HANDLE schService;
    // Путь к исполняемому файлу службы (dev-win-sc.exe, который мы сначала запускаем как консольное приложение)
    TCHAR szUnquotedPath[MAX_PATH];

    // Структура полного описания службы
    SERVICE_DESCRIPTION sd;

    // Преобразование полного описания службы в LPTSTR
    LPTSTR szDesc = WITHOUT_CONST(SVCDESCRIPTION);

    // Определение пути до текущего исполняемого файла (dev-win-sc.exe)
    if (!GetModuleFileName(NULL, szUnquotedPath, MAX_PATH))
    {
        logger << (LogMsg() << "Не удалось установить службу (" << GetLastError() << ")");
        return;
    }

    // В случае, если путь содержит пробел, его необходимо заключить в кавычки, чтобы
    // он был правильно интерпретирован. Например,
    // "d:\my share\myservice.exe" следует указывать как
    // ""d:\my share\myservice.exe""
    TCHAR szPath[MAX_PATH];
    StringCbPrintf(szPath, MAX_PATH, TEXT("\"%s\""), szUnquotedPath);

    logger << (LogMsg() << "Путь к исполняемому файлу службы: " << szPath);

    // Получение дескриптора менеджера управления службами
    schSCManager = OpenSCManager(
        NULL,                   // Имя компьютера
        NULL,                   // Имя конкретной базы данных служб
        SC_MANAGER_ALL_ACCESS); // Права доступа (указываем все права)

    if (schSCManager == NULL)
    {
        logger << (LogMsg() << "OpenSCManager вернул NULL (" << GetLastError() << ")");
        return;
    }

    // Создание службы и получение её дескриптора
    schService = CreateServiceA(
        schSCManager,              // Дескриптор SCM
        SVCNAME,                   // Имя службы (отображается в диспетчере устройств)
        SVCDISPLAY,                // Краткое описание службы (отображается в диспетчере устройств и окне "Службы")
        SERVICE_ALL_ACCESS,        // Определение прав для службы (полный доступ)
        SERVICE_WIN32_OWN_PROCESS, // Тип службы
        SERVICE_DEMAND_START,      // Тип запуска
        SERVICE_ERROR_NORMAL,      // Тип контроля ошибки
        szPath,                    // Путь до исполняемого файла службы
        NULL,                      // Группа
        NULL,                      // Тег идентификатора
        NULL,                      // Зависимости
        NULL,                      // Стартовое имя (LocalSystem)
        NULL);                     // Пароль

    if (schService == NULL)
    {
        logger << (LogMsg() << "CreateService вернул NULL (" << GetLastError() << ")");

        // Закрытие дескриптора SCM
        CloseServiceHandle(schSCManager);
        return;
    }
    else
    {
        logger << "Служба успешно установлена";

        // Добавляем полное описание службы
        sd.lpDescription = szDesc;

        // Инициируем операцию изменения полного описания службы, которое сейчас есть в базе данных служб
        if (!ChangeServiceConfig2(
                schService,
                SERVICE_CONFIG_DESCRIPTION,
                &sd))
        {
            logger << "ChancheServiceConfig2 вернул NULL";
        }
        else
        {
            logger << "Полное описание службы успешно установлено";
        }
    }

    // Закрытие дескриптора службы и SCM
    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);
}

Теперь пришла пора добраться до точки входа в нашу программу.

Для начала отметим, что наша программа будет запускаться как консольное приложение только в тех случаях, когда нужно управлять службой, а не выполнять её роль. То есть, наша программа может работать как консольное приложение, но как служба — нет, мы её не сможем сами запустить как службу. Как служба её будет запускать SCM по нашей команде, которую мы отправим через консольное приложение.

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

Изменим точку входа в приложение следующим образом:

// Точка входа в приложение
int __cdecl _tmain(int argc, TCHAR *argv[])
{
    // Добавление поддержки русского языка в консоли
    setlocale(LC_ALL, "ru");

    if (lstrcmpi(argv[1], TEXT("/install")) == 0)
    {
        logger << "Запуск установки службы ...";

        // Вызов функции установки службы если был передан аргумент /install
        SvcInstall();
        return 0;
    }

    return 0;
}

Функция lstrcmpi просто сравнивает строки. Первым аргументом argv[0] всегда будет идти имя программы, а уже начиная с arg[1] — аргументы для запуска программы.

Теперь нужно осуществить сборку программы и запустить её через консоль CMD с правами администратора (это важно) используя следующую команду:

dev-win-sc.exe /install

Выполнить данную команду необходимо только там, где расположен файл dev-win-sc.exe. Обычно он расположен в директории dev-win-sc\out\build\x**-debug, но если настройки CMake поменять, то путь до исполняемого файла может быть другим.

Рисунок 9 - Результат установки службы

Рисунок 9 — Результат установки службы

После установки службы в диспетчере задач и окне «Службы» её может быть не видно сразу, поэтому может сложиться впечатление что установка на самом деле не удалась, однако после перезагрузки этих окон данная служба появится:

Рисунок 10 - Вид службы из диспетчера задач

Рисунок 10 — Вид службы из диспетчера задач
Рисунок 11 - Вид службы из окна "Службы"

Рисунок 11 — Вид службы из окна «Службы»

На рисунке 11 можно отметить, что настройки, применяемые при создании службы, были отражены в окне «Службы». Поскольку мы устанавливали стартовое имя (вход по имени) как LocalSystem, а тип запуска как SERVICE_DEMAND_START (Вручную).

Также, если мы теперь посмотрим более детальную информацию об этой службе через PowerShell, то можем обнаружить интересную информацию:

Рисунок 12 - Информация о службе

Рисунок 12 — Информация о службе

ExitCode 1077 службы в Windows означает, что с момента последней загрузки службы попытки её запустить не предпринимались. То есть она была загружена, но не была запущена. Такое справедливо для всех служб в Windows:

Рисунок 13 - BITS был запущен, но остановлен, а aspnet_state с момента загрузки в систему так и не был запущен и всегда оставался остановленным

Рисунок 13 — BITS был запущен, но остановлен, а aspnet_state с момента загрузки в систему так и не был запущен и всегда оставался остановленным

Что ж, самое время научится запускать службу, чтобы она выполняла какую-нибудь полезную нагрузку.

Реализация функции запуска службы

Начнём с добавления объявления функции DoStartSvc в заголовочный файл WinService.h:

// Запуск службы
VOID __stdcall DoStartSvc(void);

__stdcall — это специальное соглашение о вызовах, которое применяется в ОС Windows для вызова функций WinAPI.

Особенности данного соглашения следующие:

  1. Аргументы функций передаются через стек, справа налево;

  2. Очистку стека производит вызываемая программа;

  3. Возвращаемое значение записывается в регистр EAX.

Теперь приступим к последовательной реализации функции DoStartSvc в файле WinService.cpp.

Для начала добавим переменную для хранения информации о статусе службы, хранения времени в миллисекундах, переменные для вычисления времени ожидания и числа необходимых байт (она понадобится при получении статуса процесса службы), а также дескриптор SCM и дескриптор службы:

// Информация о статусе службы
SERVICE_STATUS_PROCESS ssStatus;
// Переменные для хранения времени в миллисекундах
ULONGLONG dwOldCheckPoint;
ULONGLONG dwStartTickCount;

// Вычисляемое время ожидания
DWORD dwWaitTime;
// Число необходимых байт
DWORD dwBytesNeeded;

// Дескриптор SCM
SC_HANDLE schSCManager;

// Дескриптор службы
SC_HANDLE schService;

Теперь обращаемся к SCM и получаем сначала его дескриптор, а затем и дескриптор загруженной в базе данных службы:

// Получение дескриптора SCM
schSCManager = OpenSCManager(
    NULL,                   // Имя компьютера
    NULL,                   // Название базы данных
    SC_MANAGER_ALL_ACCESS); // Полный доступ

if (NULL == schSCManager)
{
    logger << (LogMsg() << "OpenSCManager вернул NULL (" << GetLastError() << ")");
    return;
}

// Получение дескриптора службы
schService = OpenService(
    schSCManager,        // Дескриптор SCM
    SVCNAME,             // Имя службы
    SERVICE_ALL_ACCESS); // Полный доступ

if (schService == NULL)
{
    logger << (LogMsg() << "OpenService вернул NULL (" << GetLastError() << ")");

    // Закрытие дескриптора SCM
    CloseServiceHandle(schSCManager);
    return;
}

Теперь нам необходимо получить текущий статус службы. Сделать это можно с помощью функции QueryServiceStatusEx:

// Получение статуса службы
if (!QueryServiceStatusEx(
        schService,                     // Дескриптор службы
        SC_STATUS_PROCESS_INFO,         // Уровень требуемой информации
        (LPBYTE)&ssStatus,              // Адрес структуры
        sizeof(SERVICE_STATUS_PROCESS), // Размер структуры
        &dwBytesNeeded))                // Необходимый размер, если буфер слишком мал
{
    logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");

    // Закрытие дескрипторов
    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);
    return;
}

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

// Проверяем, запущена ли служба уже. Если она запущена, то нет смысла её запускать ещё раз.
if (ssStatus.dwCurrentState != SERVICE_STOPPED && ssStatus.dwCurrentState != SERVICE_STOP_PENDING)
{
    logger << "Нельзя запустить службу, поскольку она уже запущена";

    // Закрытие дескрипторов
    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);
    return;
}

Если служба не остановлена и не пытается остановится, тогда служба уже запущена и работает в системе, значит запускать её снова нет смысла.

После того, как мы актуализировали статус службы, теперь необходимо дождаться завершения статуса SERVICE_STOP_PENDING, который означает что служба только готовится остановится. Раз она готовится остановится, то имеет смысл дождаться, пока она остановится окончательно и завершит свою работу, а потом её снова запустить, т.к. мы реализуем функцию запуска службы.

// Сохраняем количество тиков в начальную точку
dwStartTickCount = GetTickCount64();

// Старое значение контрольной точки (ориентируемся на предыдущий запуск службы)
dwOldCheckPoint = ssStatus.dwCheckPoint;

// Дожидаемся остановки службы прежде чем запустить её
while (ssStatus.dwCurrentState == SERVICE_STOP_PENDING)
{
    // Не ждём дольше, чем указано в подсказке "Ждать" (dwWaitHint). Оптимальный интервал составляет
    // одну десятую от указанного в подсказке, но не менее 1 секунды и не более 10 секунд
    dwWaitTime = ssStatus.dwWaitHint / 10;

    if (dwWaitTime < 1000)
    {
        dwWaitTime = 1000;
    }
    else if (dwWaitTime > 10000)
    {
        dwWaitTime = 10000;
    }

    Sleep(dwWaitTime);
    // Проверяем статус до тех пор, пока служба больше не перестанет находится в режиме ожидания
    if (!QueryServiceStatusEx(
            schService,                     
            SC_STATUS_PROCESS_INFO,         
            (LPBYTE)&ssStatus,              
            sizeof(SERVICE_STATUS_PROCESS), 
            &dwBytesNeeded))                
    {
        logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");

        // Закрытие дескрипторов
        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }

    if (ssStatus.dwCheckPoint > dwOldCheckPoint)
    {
        // Продолжаем ждать и проверять
        dwStartTickCount = GetTickCount64();
        dwOldCheckPoint = ssStatus.dwCheckPoint;
    }
    else
    {
        if ((GetTickCount64() - dwStartTickCount) > (ULONGLONG)ssStatus.dwWaitHint)
        {
            logger << "Таймаут ожидания остановки службы";

            CloseServiceHandle(schService);
            CloseServiceHandle(schSCManager);
            return;
        }
    }
}

После того, как мы точно убедились что служба завершила свою работу (статус SERVICE_STOP_PENDING поменялся на SERVICE_STOP), можно приступать к запуску службы используя функцию StartService:

// Отправляем запрос на запуск службы
if (!StartService(
        schService, // Дескриптор службы
        0,          // Число аргументов
        NULL))      // Отсутствуют аргументы
{
    logger << (LogMsg() << "StartService вернул NULL (" << GetLastError() << ")");

    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);
    return;
}
else
{
    logger << "Служба в процессе запуска ...";
}

Обратите внимание, что когда успешно был запрошен запуск службы (StartService не вернул NULL), то состояние службы в этот момент времени должно быть SERVICE_START_PENDING, а не SERVICE_START как мы могли бы ожидать, и именно поэтому необходимо ещё раз осуществить ожидание смены статуса, но на этот раз уже с состояния SERVICE_START_PENDING на SERVICE_START:

// Получаем статус службы
if (!QueryServiceStatusEx(
        schService,                     
        SC_STATUS_PROCESS_INFO,         
        (LPBYTE)&ssStatus,              
        sizeof(SERVICE_STATUS_PROCESS), 
        &dwBytesNeeded))                
{
    logger << (LogMsg() << "QueryServiceStatusEx вернул (" << GetLastError() << ")");

    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);
    return;
}

// Сохраняем количество тиков в начальную точку
dwStartTickCount = GetTickCount64();
dwOldCheckPoint = ssStatus.dwCheckPoint;

while (ssStatus.dwCurrentState == SERVICE_START_PENDING)
{
    // Не ждём дольше, чем указано в подсказке "Ждать" (dwWaitHint). Оптимальный интервал составляет
    // одну десятую от указанного в подсказке, но не менее 1 секунды и не более 10 секунд
    dwWaitTime = ssStatus.dwWaitHint / 10;

    if (dwWaitTime < 1000)
    {
        dwWaitTime = 1000;
    }
    else if (dwWaitTime > 10000)
    {
        dwWaitTime = 10000;
    }

    Sleep(dwWaitTime);

    // Дополнительная проверка статуса
    if (!QueryServiceStatusEx(
            schService,                    
            SC_STATUS_PROCESS_INFO,         
            (LPBYTE)&ssStatus,              
            sizeof(SERVICE_STATUS_PROCESS), 
            &dwBytesNeeded))                
    {
        logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");
        break;
    }

    if (ssStatus.dwCheckPoint > dwOldCheckPoint)
    {
        // Продолжаем проверку
        dwStartTickCount = GetTickCount64();
        dwOldCheckPoint = ssStatus.dwCheckPoint;
    }
    else
    {
        if ((GetTickCount64() - dwStartTickCount) > (ULONGLONG)ssStatus.dwWaitHint)
        {
            // Не было достигнуто никакого прогресса (подсказка "Подождать" стала не актуальной)
            break;
        }
    }
}

// Определяем запущена ли служба
if (ssStatus.dwCurrentState == SERVICE_RUNNING)
{
    logger << "Служба успешно запущена";
}
else
{
    logger << "Служба не запущена";
    logger << (LogMsg() << "Current state: " << ssStatus.dwCurrentState);
    logger << (LogMsg() << "Exit Code: " << ssStatus.dwWin32ExitCode);
    logger << (LogMsg() << "Check Point: " << ssStatus.dwCheckPoint);
    logger << (LogMsg() << "Wait Hint: " << ssStatus.dwWaitHint);
}

CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);

На этом функция запуска службы успешно реализована.

Полный код определения функции DoStartSvc

/* Функция запуска службы */
VOID __stdcall DoStartSvc()
{
    // Информация о статусе службы
    SERVICE_STATUS_PROCESS ssStatus;
    // Переменные для хранения времени в миллисекундах
    ULONGLONG dwOldCheckPoint;
    ULONGLONG dwStartTickCount;

    // Вычисляемое время ожидания
    DWORD dwWaitTime;
    // Число необходимых байт
    DWORD dwBytesNeeded;

    // Дескриптор SCM
    SC_HANDLE schSCManager;

    // Дескриптор службы
    SC_HANDLE schService;

    // Получение дескриптора SCM
    schSCManager = OpenSCManager(
        NULL,                   // Имя компьютера
        NULL,                   // Название базы данных
        SC_MANAGER_ALL_ACCESS); // Полный доступ

    if (NULL == schSCManager)
    {
        logger << (LogMsg() << "OpenSCManager вернул NULL (" << GetLastError() << ")");
        return;
    }

    // Получение дескриптора службы
    schService = OpenService(
        schSCManager,        // Дескриптор SCM
        SVCNAME,             // Имя службы
        SERVICE_ALL_ACCESS); // Полный доступ

    if (schService == NULL)
    {
        logger << (LogMsg() << "OpenService вернул NULL (" << GetLastError() << ")");

        // Закрытие дескриптора SCM
        CloseServiceHandle(schSCManager);
        return;
    }

    // Получение статуса службы
    if (!QueryServiceStatusEx(
            schService,                     // Дескриптор службы
            SC_STATUS_PROCESS_INFO,         // Уровень требуемой информации
            (LPBYTE)&ssStatus,              // Адрес структуры
            sizeof(SERVICE_STATUS_PROCESS), // Размер структуры
            &dwBytesNeeded))                // Необходимый размер, если буфер слишком мал
    {
        logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");

        // Закрытие дескрипторов
        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }


    // Проверяем, запущена ли служба уже. Если она запущена, то нет смысла её запускать ещё раз.
    if (ssStatus.dwCurrentState != SERVICE_STOPPED && ssStatus.dwCurrentState != SERVICE_STOP_PENDING)
    {
        logger << "Нельзя запустить службу, поскольку она уже запущена";

        // Закрытие дескрипторов
        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }

    // Сохраняем количество тиков в начальную точку
    dwStartTickCount = GetTickCount64();

    // Старое значение контрольной точки (ориентируемся на предыдущий запуск службы)
    dwOldCheckPoint = ssStatus.dwCheckPoint;

    // Дожидаемся остановки службы прежде чем запустить её
    while (ssStatus.dwCurrentState == SERVICE_STOP_PENDING)
    {
        // Не ждём дольше, чем указано в подсказке "Ждать" (dwWaitHint). Оптимальный интервал составляет
        // одну десятую от указанного в подсказке, но не менее 1 секунды и не более 10 секунд
        dwWaitTime = ssStatus.dwWaitHint / 10;

        if (dwWaitTime < 1000)
        {
            dwWaitTime = 1000;
        }
        else if (dwWaitTime > 10000)
        {
            dwWaitTime = 10000;
        }

        Sleep(dwWaitTime);

        // Проверяем статус до тех пор, пока служба больше не перестанет находится в режиме ожидания
        if (!QueryServiceStatusEx(
                schService,                     
                SC_STATUS_PROCESS_INFO,         
                (LPBYTE)&ssStatus,              
                sizeof(SERVICE_STATUS_PROCESS), 
                &dwBytesNeeded))                
        {
            logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");

            // Закрытие дескрипторов
            CloseServiceHandle(schService);
            CloseServiceHandle(schSCManager);
            return;
        }

        if (ssStatus.dwCheckPoint > dwOldCheckPoint)
        {
            // Продолжаем ждать и проверять
            dwStartTickCount = GetTickCount64();
            dwOldCheckPoint = ssStatus.dwCheckPoint;
        }
        else
        {
            if ((GetTickCount64() - dwStartTickCount) > (ULONGLONG)ssStatus.dwWaitHint)
            {
                logger << "Таймаут ожидания остановки службы";

                CloseServiceHandle(schService);
                CloseServiceHandle(schSCManager);
                return;
            }
        }
    }

    // Отправляем запрос на запуск службы
    if (!StartService(
            schService, // Дескриптор службы
            0,          // Число аргументов
            NULL))      // Отсутствуют аргументы
    {
        logger << (LogMsg() << "StartService вернул NULL (" << GetLastError() << ")");

        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }
    else
    {
        logger << "Служба в процессе запуска ...";
    }

    // Получаем статус службы
    if (!QueryServiceStatusEx(
            schService,                     
            SC_STATUS_PROCESS_INFO,         
            (LPBYTE)&ssStatus,              
            sizeof(SERVICE_STATUS_PROCESS), 
            &dwBytesNeeded))                
    {
        logger << (LogMsg() << "QueryServiceStatusEx вернул (" << GetLastError() << ")");

        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }

    // Сохраняем количество тиков в начальную точку
    dwStartTickCount = GetTickCount64();
    dwOldCheckPoint = ssStatus.dwCheckPoint;

    while (ssStatus.dwCurrentState == SERVICE_START_PENDING)
    {
        // Не ждём дольше, чем указано в подсказке "Ждать" (dwWaitHint). Оптимальный интервал составляет
        // одну десятую от указанного в подсказке, но не менее 1 секунды и не более 10 секунд
        dwWaitTime = ssStatus.dwWaitHint / 10;

        if (dwWaitTime < 1000)
        {
            dwWaitTime = 1000;
        }
        else if (dwWaitTime > 10000)
        {
            dwWaitTime = 10000;
        }

        Sleep(dwWaitTime);

        // Дополнительная проверка статуса
        if (!QueryServiceStatusEx(
                schService,                    
                SC_STATUS_PROCESS_INFO,         
                (LPBYTE)&ssStatus,              
                sizeof(SERVICE_STATUS_PROCESS), 
                &dwBytesNeeded))                
        {
            logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");
            break;
        }

        if (ssStatus.dwCheckPoint > dwOldCheckPoint)
        {
            // Продолжаем проверку
            dwStartTickCount = GetTickCount64();
            dwOldCheckPoint = ssStatus.dwCheckPoint;
        }
        else
        {
            if ((GetTickCount64() - dwStartTickCount) > (ULONGLONG)ssStatus.dwWaitHint)
            {
                // Не было достигнуто никакого прогресса (подсказка "Подождать" стала не актуальной)
                break;
            }
        }
    }

    // Определяем запущена ли служба
    if (ssStatus.dwCurrentState == SERVICE_RUNNING)
    {
        logger << "Служба успешно запущена";
    }
    else
    {
        logger << "Служба не запущена";
        logger << (LogMsg() << "Current state: " << ssStatus.dwCurrentState);
        logger << (LogMsg() << "Exit Code: " << ssStatus.dwWin32ExitCode);
        logger << (LogMsg() << "Check Point: " << ssStatus.dwCheckPoint);
        logger << (LogMsg() << "Wait Hint: " << ssStatus.dwWaitHint);
    }

    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);
}

Поскольку теперь служба может запускаться, то нужно сделать так, чтобы исполняемый файл мог запускаться и без аргументов (потому что наша служба будет его запускать таким образом).

Для этого уже на данном этапе необходимо добавить функции, которые будут выполнять полезную нагрузку службы и останавливать её когда это необходимо.

Прежде всего вернёмся в файл dev-win-sc.cpp и добавим выше функции _tmain следующее объявление функций:

VOID SvcInit(DWORD, LPTSTR *);
VOID WINAPI SvcMain(DWORD, LPTSTR *);

Функция SvcMain является точкой входа для службы (да, это не функция _tmain, она лишь точка входа в приложение для управления этой службой).

Ниже функции _tmain разместим её определение, которое последовательно сейчас рассмотрим.

Для начала, определим функцию SvcMain:

/* Точка входа в службу */
VOID WINAPI SvcMain(DWORD dwArgc, LPTSTR *lpszArgv)
{
  // Код точки входа ...
}

Сигнатура данной функции очень похожа на сигнатуру функции _tmain, поскольку и там, и тут есть указание на число аргументов, передаваемой функции SvcMain (dwArgc) и сами аргументы, которые ей переданы (lpszArgv). В общем, это полноценная точка входа в службу (по сигнатуре).

Поскольку служба будет работать в фоновом режиме и мы после её запуска не контролируем приложение (в том числе когда её остановить, здесь Ctrl + C не помощник), то стоит добавить глобальные данные о службе, которые будут помогать всем функциям службы выполнятся последовательно и/или не выполнятся совсем.

Начнём с добавления структуры статуса службы, дескриптора статуса службы и дескриптора, который будет использоваться в качестве флага, изменение которого будут отражены на дальнейшем выполнении в файл WinService.h:

// Состояние службы
extern SERVICE_STATUS          gSvcStatus;
// Дескриптор состояния службы
extern SERVICE_STATUS_HANDLE   gSvcStatusHandle;
// Дескриптор флага остановки работы службы
extern HANDLE                  ghSvcStopEvent;

В файле WinService.cpp не забываем добавить реализацию данных переменных (иначе может быть ошибка):

SERVICE_STATUS gSvcStatus;
SERVICE_STATUS_HANDLE gSvcStatusHandle;
HANDLE ghSvcStopEvent = NULL;

Теперь сделаем объявление функции в файле WinService.h, которая будет обрабатывать все служебные сообщения от SCM в текущей службе:

/* Обработчик служебных сообщений от SCM */
VOID WINAPI SvcCtrlHandler(DWORD);

А её определение разместим в WinService.cpp:

/* Вызывается SCM всякий раз, когда в службу отправляется управляющий код с помощью функции ControlService */
VOID WINAPI SvcCtrlHandler(DWORD dwCtrl)
{
    // Обработка полученного управляющего кода
    switch (dwCtrl)
    {
    case SERVICE_CONTROL_STOP:
        // Сигнализируем SCM о том, что текущая служба находится на этапе подготовки к остановке
        ReportSvcStatus(SERVICE_STOP_PENDING, NO_ERROR, 0);

        // Сигнализируем службу об остановке (просто меняем значение дескриптора ghSvcStopEvent)
        SetEvent(ghSvcStopEvent);

        // Сигнализируем SCM о завершении работы службы с определённым состоянием
        ReportSvcStatus(gSvcStatus.dwCurrentState, NO_ERROR, 0);

        logger << "SERVICE_CONTROL_STOP";
        return;

    case SERVICE_CONTROL_INTERROGATE:
        logger << "SERVICE_CONTROL_INTERROGATE";
        break;

    default:
        break;
    }
}

Функция SvcCtrlHandler будет вызвана всякий раз, когда в нашу службу будет отправляться управляющий код SCM.

Код SERVICE_CONTROL_STOP отправляется в службу тогда, когда службу нужно остановить (поступила такая команда от SCM и её надо обработать).

В ходе обработки остановки службы мы периодически отправляем текущий статус службы SCM через функцию ReportSvcStatus, которую мы ещё не реализовали, а с помощью функции SetEvent мы меняем значение дескриптора ghSvcStopEvent для того, чтобы завершить работу нашей службы (пока это может быть неясно — собственно зачем это делаем? Но по ходу доработки функции SvcMain и SvcInit будет ясно, почему именно данный дескриптор отвечает за остановку службы).

Что ж, теперь реализуем функцию ReportSvcStatus. В WinService.h добавим её объявление:

/* Сообщение текущего статуса службы SCM */
VOID ReportSvcStatus(DWORD, DWORD, DWORD);

Теперь разберём пошагово определение данной функции в файле WinService.cpp.

Для начала определяем значение контрольной точки, устанавливаем текущее состояние службы, текущего ExitCode и расчётное время ожидания операции в миллисекундах:

// Определение значения контрольной точки
static DWORD dwCheckPoint = 1;

// Установка текущего состояния службы
gSvcStatus.dwCurrentState = dwCurrentState;
// Установка ExitCode службы
gSvcStatus.dwWin32ExitCode = dwWin32ExitCode;
// Установка рассчётного времени ожидания операции в миллисекундах
gSvcStatus.dwWaitHint = dwWaitHint;

Затем проверяем переданное состояние на значение SERVICE_START_PENDING:

// Проверка на подготовку сервиса к запуску
if (dwCurrentState == SERVICE_START_PENDING)
{
    gSvcStatus.dwControlsAccepted = 0;
}
else
{
    // Служба обрабатывает команду остановки
    gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
}

Поле dwControlsAccepted из структуры SERVICE_STATUS, которое определяет какие команды управления будут обрабатываться или уже обрабатываются службой. В данном случае мы ставим это значение либо как 0, либо как SERVICE_ACCEPT_STOP. Это можно понимать как то, что мы уведомляем SCM о том, что служба может обрабатывать команду завершения только тогда, когда она не готовится к запуску. В противном случае она может обрабатывать событие завершения.

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

// Если служба запущена или остановлена сбрасываем значение контрольной точки
if ((dwCurrentState == SERVICE_RUNNING) ||
    (dwCurrentState == SERVICE_STOPPED))
{
    gSvcStatus.dwCheckPoint = 0;
}
else
{
    // Иначе добавляем значение контрольной точки 
    gSvcStatus.dwCheckPoint = dwCheckPoint++;
}

// Отправка текущего статуса службы в SCM
SetServiceStatus(gSvcStatusHandle, &gSvcStatus);

Полный код определения функции ReportSvcStatus

/* Устанавливает текущий статус обслуживания и сообщает о нем в SCM */
VOID ReportSvcStatus(
    DWORD dwCurrentState,
    DWORD dwWin32ExitCode,
    DWORD dwWaitHint)
{
    // Определение значения контрольной точки
    static DWORD dwCheckPoint = 1;

    // Установка текущего состояния службы
    gSvcStatus.dwCurrentState = dwCurrentState;
    // Установка ExitCode службы
    gSvcStatus.dwWin32ExitCode = dwWin32ExitCode;
    // Установка расчётного времени ожидания операции в миллисекундах
    gSvcStatus.dwWaitHint = dwWaitHint;

    // Проверка на подготовку сервиса к запуску
    if (dwCurrentState == SERVICE_START_PENDING)
    {
        gSvcStatus.dwControlsAccepted = 0;
    }
    else
    {
        // Служба обрабатывает команду остановки
        gSvcStatus.dwControlsAccepted = SERVICE_ACCEPT_STOP;
    }

    // Если служба запущена или остановлена сбрасываем значение контрольной точки
    if ((dwCurrentState == SERVICE_RUNNING) ||
        (dwCurrentState == SERVICE_STOPPED))
    {
        gSvcStatus.dwCheckPoint = 0;
    }
    else
    {
        // Иначе добавляем значение контрольной точки 
        gSvcStatus.dwCheckPoint = dwCheckPoint++;
    }

    // Отправка текущего статуса службы в SCM
    SetServiceStatus(gSvcStatusHandle, &gSvcStatus);
}

Теперь в SvcCtrlHandler известно о такой функции как ReportSvcStatus, а значит никаких ошибок быть не должно.

Наконец мы можем снова вернуться к реализации функции SvcMain, поскольку у нас теперь есть функция для обработки завершения работы службы и функция для отправки в SCM текущего статуса службы:

/* Точка входа в службу */
VOID WINAPI SvcMain(DWORD dwArgc, LPTSTR *lpszArgv)
{
  // Код точки входа ...
}

Точка входа в службу будет начинаться с регистрации обработчика сообщений, полученных от SCM:

// Регистрация обработчика управляющих сообщений для службы
gSvcStatusHandle = RegisterServiceCtrlHandler(
    SVCNAME,        // Имя службы
    SvcCtrlHandler);// Обработчик

if (!gSvcStatusHandle)
{
    logger << (LogMsg() << "Не получилось зарегистрировать обработчик для службы (" << GetLastError() << ")");
    return;
}

Регистрация обработчика происходит с помощью функции RegisterServiceCtrlHandler. Ей передаётся имя службы и функция-обработчик.

Далее определим значения в структуре службы и зададим ей начальное состояние как SERVICE_START_PENDING (находится в процессе запуска), с установкой расчётного времени ожидания операции как 3 секунды:

// Определяем значение в структуре статуса службы
gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; // Тип службы
gSvcStatus.dwServiceSpecificExitCode = 0;             // ExitCode

logger << "Служба будет находится в состоянии SERVICE_START_PENDING в течении 3 секунд";

// Установка начального состояния службы
ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);

logger << "Запуск функции SvcInit";
SvcInit(dwArgc, lpszArgv);

Полный код определения функции SvcMain

/* Точка входа в службу */
VOID WINAPI SvcMain(DWORD dwArgc, LPTSTR *lpszArgv)
{
    // Регистрация обработчика управляющих сообщений для службы
    gSvcStatusHandle = RegisterServiceCtrlHandler(
        SVCNAME,        // Имя службы
        SvcCtrlHandler);// Обработчик

    if (!gSvcStatusHandle)
    {
        logger << (LogMsg() << "Не получилось зарегистрировать обработчик для службы (" << GetLastError() << ")");
        return;
    }

    // Определяем значение в структуре статуса службы
    gSvcStatus.dwServiceType = SERVICE_WIN32_OWN_PROCESS; // Тип службы
    gSvcStatus.dwServiceSpecificExitCode = 0;             // ExitCode

    logger << "Служба будет находится в состоянии SERVICE_START_PENDING в течении 3 секунд";
    ReportSvcStatus(SERVICE_START_PENDING, NO_ERROR, 3000);

    logger << "Запуск функции SvcInit";
    SvcInit(dwArgc, lpszArgv);
}

И, наконец, займёмся реализацией функции SvcInit, в которой и будет происходить самое интересное — основная работа нашей службы.

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

// Создание события, которое будет сигнализировать об остановке службы
ghSvcStopEvent = CreateEvent(
    NULL,  // Аттрибуты события
    TRUE,  // Ручной сброс события
    FALSE, // Не сигнализировать сразу после создания события
    NULL); // Имя события

if (ghSvcStopEvent == NULL)
{
    DWORD lastError = GetLastError();
    logger << (LogMsg() << "CreateEvent вернул NULL (" << lastError << ")");

    // Отправка в SCM состояния об остановке службы
    ReportSvcStatus(SERVICE_STOPPED, GetLastError(), 0);
    return;
}

ghSvcStopEvent — это дескриптор события, с которым мы уже сталкивались ранее (он определён в файле WinService.h).

Осталось добавить отправку статуса SERVICE_RUNNING в SCM и реализовать бесконечный цикл службы, чтобы она работала до тех пор, пока значения дескриптора события не перейдёт в сигнальное состояние (данная проверка реализована с помощью функции WaitForSingleObject):

// Отправка статуса SCM о работе службы
ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
logger << "Работа службы";

int count = 1;

// Работа бесконечного цикла до тех пор, пока не будет подан сигнал о завершении работы службы
while (WaitForSingleObject(ghSvcStopEvent, 0) != WAIT_OBJECT_0)
{
    logger << (LogMsg() << "[WORKING] Счётчик: " << count++);
    Sleep(2000);
}

logger << "Завершение работы службы";
// Отправка статуса SCM о завершении службы
ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);

Полный код определения функции SvcInit

/* Функция, в которой описана основная работа службы */
VOID SvcInit(DWORD dwArgc, LPTSTR *lpszArgv)
{
    // Создание события, которое будет сигнализировать об остановке службы
    ghSvcStopEvent = CreateEvent(
        NULL,  // Аттрибуты события
        TRUE,  // Ручной сброс события
        FALSE, // Не сигнализировать сразу после создания события
        NULL); // Имя события

    if (ghSvcStopEvent == NULL)
    {
        DWORD lastError = GetLastError();

        logger << (LogMsg() << "CreateEvent вернул NULL (" << lastError << ")");

        // Отправка в SCM состояния об остановке службы
        ReportSvcStatus(SERVICE_STOPPED, GetLastError(), 0);
        return;
    }

    // Отправка статуса SCM о работе службы
    ReportSvcStatus(SERVICE_RUNNING, NO_ERROR, 0);
    logger << "Работа службы";

    int count = 1;

    // Работа бесконечного цикла до тех пор, пока не будет подан сигнал о завершении работы службы
    while (WaitForSingleObject(ghSvcStopEvent, 0) != WAIT_OBJECT_0)
    {
        logger << (LogMsg() << "[WORKING] Счётчик: " << count++);
    }

    logger << "Завершение работы службы";
    // Отправка статуса SCM о завершении службы
    ReportSvcStatus(SERVICE_STOPPED, NO_ERROR, 0);
}

Теперь изменим точку входа в приложение следующим образом:

// Точка входа в приложение
int __cdecl _tmain(int argc, TCHAR *argv[])
{
    // Добавление поддержки русского языка в консоли
    setlocale(LC_ALL, "ru");

    if (lstrcmpi(argv[1], TEXT("/install")) == 0)
    {
        logger << "Запуск установки службы ...";

        // Вызов функции установки службы если был передан аргумент /install
        SvcInstall();
        return 0;
    }
    else if (lstrcmpi(argv[1], TEXT("/start")) == 0)
    {
        logger << "Запуск службы ...";

        DoStartSvc();
        return 0;
    }
    else
    {
        // Описание точки входа для SCM
        SERVICE_TABLE_ENTRY DispatchTable[] =
            {
                {WITHOUT_CONST(SVCNAME), (LPSERVICE_MAIN_FUNCTION)SvcMain},
                {NULL, NULL}};

        logger << "Запуск StartServiceCtrlDispatcher...";
        if (!StartServiceCtrlDispatcher(DispatchTable))
        {
            DWORD lastError = GetLastError();

            switch (lastError)
            {
            case ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:
            {
                logger << "ERROR_FAILED_SERVICE_CONTROLLER_CONNECT";
                break;
            }
            case ERROR_INVALID_DATA:
            {
                logger << "ERROR_INVALID_DATA";
                break;
            }
            case ERROR_SERVICE_ALREADY_RUNNING:
            {
                logger << "ERROR_SERVICE_ALREADY_RUNNING";
                break;
            }
            }
        }
    }

    return 0;
}

Как можно заметить изменений в ней произошло много. Начнём по порядку.

Первое что мы добавили — обработку аргумента /start, который передаётся приложению. Если данный аргумент был передан приложению, то запускается функция DoSvcStart, которая и запускает нашу службу ориентируясь на её описание в базе данных служб (имени и пути к исполняемому файлу).

Также добавлена секция else, которая необходима для запуска службы SCM. Данная секция выполняется тогда, когда параметров никаких не было передано приложению. В данной секции всё довольно просто. Сначала мы описываем точку входа для SCM, а затем вызываем функцию StartServiceCtrlDispatcher, с помощью которой и происходит запуск точки входа в службу.

Функция StartServiceCtrlDispatcher соединяет основной поток процесса службы с SCM, в результате чего этот поток становится потоком SCM и работает в его контексте.

Пояснение: когда SCM запускает процесс службы, он ожидает, пока из основного потока процесса службы не будет вызвана функция StartServiceCtrlDispatcher. Основной поток процесса службы должен выполнить вызов данной функции как можно скорее после его запуска (в течении 30-ти секунд). Если StartServiceCtrlDispatcher завершается успешно, то результатом выполнения данной функции будет присоединение основного потока процесса службы (откуда эта функция была вызвана) к SCM и функция StartServiceCtrlDispatcher не возвращает значения до тех пор, пока все запущенные службы (SvcMain из таблицы DispatchTable) в процессе не перейдут в состояние SERVICE_STOPPED.

Теперь протестируем наши новые добавленные функции (если вы ещё не выполнили команду /install, то необходимо выполнить данный шаг, иначе вы получите ошибку 1073 — служба уже установлена).

Для запуска службы выполняем следующую команду в консоли:

dev-win-sc.exe /start
Рисунок 14 - Результат запуска службы

Рисунок 14 — Результат запуска службы

После запуска службы в диспетчере задач можно увидеть, что ей был выдан конкретный идентификатор процесса и её состояние изменилось на «Выполняется» с «Остановлено»:

Рисунок 15 - Служба выполняется

Рисунок 15 — Служба выполняется

Через PowerShell мы также можем посмотреть изменённое состояние службы:

Рисунок 16 - ExitCode изменился с 1077 на 0

Рисунок 16 — ExitCode изменился с 1077 на 0

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

В лог-файле мы можем увидеть следующую картину (я намерено остановил службу чтобы посмотреть и эту запись в логе):

Рисунок 17 - Выполнение основной работы службы

Рисунок 17 — Выполнение основной работы службы

Как видим служба успешно завершила свою работу. В диспетчере задач у неё будет состояние «Остановлено».

В функции SvcInit в бесконечный цикл определён так:

while (WaitForSingleObject(ghSvcStopEvent, 0) != WAIT_OBJECT_0)
{
    logger << (LogMsg() << "[WORKING] Счётчик: " << count++);
    Sleep(2000);
}

Если значение, передаваемое в Sleep уменьшить или совсем убрать, то цикл будет выполнятся бесконечно и очень быстро. Здесь совершенно справедливо может возникнуть вопрос, а что если функция SvcCtrlHandler не выполнится никогда, из-за того, что скорость выполнения бесконечного цикла «заблокирует» выполнения всех сторонних операций? Я задался этим вопросом сам и понял, что такой ситуации здесь возникнуть не может, и вот почему.

Если мы выведем информацию об идентификаторе текущего потока и текущего процесса в SvcCtrlHandler и SvcInit, то мы узнаем, что работают они в разных потоках, но в одном и том же процессе. А значит даже слишком быстрые и, возможно, «блокирующие» операции выполняемые в SvcInit не помешают реагировании на события от SCM.

Убедимся в этом на практике. Добавим в SvcCtrlHandler и SvcInit следующие строки:

// WinService.cpp
VOID WINAPI SvcCtrlHandler(DWORD dwCtrl)
{
    logger << (LogMsg() << "(SvcCtrlHandler) Thread ID: " << std::this_thread::get_id() << " " << GetCurrentProcessId());
    // ...
}

// dev-win-sc.cpp
VOID SvcInit(DWORD dwArgc, LPTSTR *lpszArgv)
{
    logger << (LogMsg() << "(SvcInit) Thread ID: " << std::this_thread::get_id() << " " << GetCurrentProcessId());
    // ...
}

Теперь запустим службу, немного подождём и отключим её через диспетчер задач. В лог-файл должно записаться примерно следующее:

Рисунок 18 - Процесс один и тот же, но потоки разные

Рисунок 18 — Процесс один и тот же, но потоки разные

Функция SvcInit работала в потоке с идентификатором 1296, в то время как функция SvcCtrlHandler работает в потоке с идентификатором 19728, что явно разграничивает эти две функции и гарантирует, что SvcCtrlHandler сработает всегда, даже если в SvcInit бесконечный цикл работает очень быстро.

Хорошо, когда службу можно остановить через диспетчер задач, однако было бы ещё лучше, если бы её можно было бы остановить через приложение передав ей аргумент /stop. Реализуем данную функцию в нашем приложении.

Реализация функции остановки службы

Чтобы осуществить остановку нашей службы необходимо иметь ввиду, что от нашей службы могут зависеть другие службы, а значит для начала необходимо остановить зависимые службы, а уж потом останавливать текущую.

Для реализации функции остановки зависимых служб для начала определим в файле WinService.h функцию StopDependentServices:

BOOL __stdcall StopDependentServices(SC_HANDLE&, SC_HANDLE&);

А в файле WinService.cpp добавим её определение:

BOOL __stdcall StopDependentServices(SC_HANDLE &schSCManager, SC_HANDLE &schService)
{
  // ...
}

Как можно заметить, в сигнатуру данной функции передаётся дескриптор SCM и службы. Предполагается, что в эту функцию будут отправлены уже открытые дескрипторы.

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

// Характеризует какую зависимую службу мы сейчас обрабатываем
DWORD i;
// Число необходимых байт
DWORD dwBytesNeeded;
// Счётчик
DWORD dwCount;

// Массив структур ENUM_SERVICE_STATUS, содержащих имя службы в базе данных SCM и прочие сведения
LPENUM_SERVICE_STATUS lpDependencies = NULL;
ENUM_SERVICE_STATUS ess;

// Дескриптор зависимой службы
SC_HANDLE hDepService;

// Состояние процесса службы
SERVICE_STATUS_PROCESS ssp;

ULONGLONG dwStartTime = GetTickCount64();

// 30 секундный таймаут
ULONGLONG dwTimeout = 30000;

Теперь получим требуемый размер буфера для хранения информации о зависимых службах в структуре LPENUM_SERVICE_STATUS:

// Получаем список зависимых служб от переданной службы (только активных)
// (передаём буфер нулевой длины, чтобы получить требуемый размер буфера, куда мы будем добавлять эти службы)
if (EnumDependentServices(schService, SERVICE_ACTIVE,
                          lpDependencies, 0, &dwBytesNeeded, &dwCount))
{
    // Зависимых служб нет, или есть, но они не активны на данный момент
    return TRUE;
}

Для получения списка зависимых служб используется функция EnumDependentServices.

Если зависимых служб нет, то мы просто возвращаем значение TRUE из нашей функции. Теперь продолжим определение функции с блока else и начнём мы с обработки ошибки и выделения буфера для хранения списка зависимых служб (ведь в dwBytesNeeded теперь известно сколько нам нужно байт для хранения этого списка):

else
    {
        if (GetLastError() != ERROR_MORE_DATA)
        {
            logger << "Неизвестная ошибка";
            return FALSE; // Unexpected error
        }

        // Выделяем отдельный буфер для зависимых служб с помощью функции HeapAlloc
        lpDependencies = (LPENUM_SERVICE_STATUS)HeapAlloc(
            GetProcessHeap(), HEAP_ZERO_MEMORY, dwBytesNeeded);

        if (!lpDependencies)
        {
            // Если буфер не выделился, то ничего не делаем
            return FALSE;
        }

        // ...

Выделение буфера для зависимых служб происходит с помощью функции HeapAlloc, которая позволяет выделить блок памяти из кучи. При этом нужно учитывать, что выделенная память не может быть куда-либо перемещена.

Теперь получим список зависимых служб и загрузим их в переменную lpDependencies:

        // Получаем список зависимых служб (в dwCount записывается их количество)
        if (!EnumDependentServices(schService, SERVICE_ACTIVE,
                                   lpDependencies, dwBytesNeeded, &dwBytesNeeded,
                                   &dwCount))
        {
            // Нет перечисляемых зависимых служб
            return FALSE;
        }

        // ...

Теперь осуществляем обход зависимых служб. Количество зависимых служб, удовлетворяющих условиям поиска (они должны быть активны), записано в переменную dwCount. Будем обходить все зависимые службы в цикле.

Каждый элемент зависимой службы, хранящийся в lpDependencies, содержит её имя. По этому имени мы получим её дескриптор и инициируем логику завершения этой службы. Весь программный код обхода службы выглядит следующим образом:

        // ...
        // Обходим зависимые службы
        for (i = 0; i < dwCount; i++)
        {
            // Выбираем отдельную зависимую службу
            ess = *(lpDependencies + i);

            // Получаем дескриптор зависимой службы
            // с правами только для остановки и получения её статуса
            hDepService = OpenService(schSCManager,
                                      ess.lpServiceName,
                                      SERVICE_STOP | SERVICE_QUERY_STATUS);

            if (!hDepService)
            {
                // Дескриптор зависимой службы не получен

                logger << "!hDepService";
                return FALSE;
            }

            logger << "Отправка служебного кода для остановки службы ...";

            // Отправка кода SERVICE_CONTROL_STOP
            if (!ControlService(hDepService,
                                SERVICE_CONTROL_STOP,
                                (LPSERVICE_STATUS)&ssp))
            {
                // Освобождаем дескриптор зависимой службы
                CloseServiceHandle(hDepService);
                return FALSE;
            }

            logger << "Ожидаем остановку зависимой службы ...";

            // Цикл ожидания остановки службы
            while (ssp.dwCurrentState != SERVICE_STOPPED)
            {
                // Ожидаем столько времени, скольку указано в dwWaitHint зависимой службы
                Sleep(ssp.dwWaitHint);

                // Получение статуса зависимой службы
                if (!QueryServiceStatusEx(
                        hDepService,
                        SC_STATUS_PROCESS_INFO,
                        (LPBYTE)&ssp,
                        sizeof(SERVICE_STATUS_PROCESS),
                        &dwBytesNeeded))
                {
                    // Освобождаем дескриптор зависимой службы
                    CloseServiceHandle(hDepService);
                    return FALSE;
                }

                // Если служба уже остановлена - прекращаем ожидание
                if (ssp.dwCurrentState == SERVICE_STOPPED)
                {
                    break;
                }

                // В случае, если зависимая служба не изменила своего состояния в течении dwTimeout
                // выходим из данной функции
                if ((GetTickCount64() - dwStartTime) > dwTimeout)
                {
                    // Освобождаем дескриптор зависимой службы
                    CloseServiceHandle(hDepService);
                    return FALSE;
                }
            }

            // Освобождаем дескриптор зависимой службы
            CloseServiceHandle(hDepService);
        }

        // Освобождаем занятую память под буфер
        HeapFree(GetProcessHeap(), 0, lpDependencies);
    }

    return TRUE;

Полный код определения функции StopDependentServices

/* Остановка всех зависимых служб */
BOOL __stdcall StopDependentServices(SC_HANDLE &schSCManager, SC_HANDLE &schService)
{
    // Характеризует какую зависимую службу мы сейчас обрабатываем
    DWORD i;
    // Число необходимых байт
    DWORD dwBytesNeeded;
    // Счётчик
    DWORD dwCount;

    // Массив структур ENUM_SERVICE_STATUS, содержащих имя службы в базе данных SCM и прочие сведения
    LPENUM_SERVICE_STATUS lpDependencies = NULL;
    ENUM_SERVICE_STATUS ess;

    // Дескриптор зависимой службы
    SC_HANDLE hDepService;

    // Состояние процесса службы
    SERVICE_STATUS_PROCESS ssp;

    ULONGLONG dwStartTime = GetTickCount64();

    // 30 секундный таймаут
    ULONGLONG dwTimeout = 30000;

    // Получаем список зависимых служб от переданной службы (только активных)
    // (передаём буфер нулевой длины, чтобы получить требуемый размер буфера, куда мы будем добавлять эти службы)
    if (EnumDependentServices(schService, SERVICE_ACTIVE,
                              lpDependencies, 0, &dwBytesNeeded, &dwCount))
    {
        // Зависимых служб нет, или есть, но они не активны на данный момент
        return TRUE;
    }
    else
    {
        if (GetLastError() != ERROR_MORE_DATA)
        {
            logger << "Неизвестная ошибка";
            return FALSE; // Unexpected error
        }

        // Выделяем отдельный буфер для зависимых служб с помощью функции HeapAlloc
        lpDependencies = (LPENUM_SERVICE_STATUS)HeapAlloc(
            GetProcessHeap(), HEAP_ZERO_MEMORY, dwBytesNeeded);

        if (!lpDependencies)
        {
            // Если буфер не выделился, то ничего не делаем
            return FALSE;
        }

        // Получаем список зависимых служб (в dwCount записывается их количество)
        if (!EnumDependentServices(schService, SERVICE_ACTIVE,
                                   lpDependencies, dwBytesNeeded, &dwBytesNeeded,
                                   &dwCount))
        {
            // Нет перечисляемых зависимых служб
            return FALSE;
        }

        // Обходим зависимые службы
        for (i = 0; i < dwCount; i++)
        {
            // Выбираем отдельную зависимую службу
            ess = *(lpDependencies + i);

            // Получаем дескриптор зависимой службы
            // с правами только для остановки и получения её статуса
            hDepService = OpenService(schSCManager,
                                      ess.lpServiceName,
                                      SERVICE_STOP | SERVICE_QUERY_STATUS);

            if (!hDepService)
            {
                // Дескриптор зависимой службы не получен

                logger << "!hDepService";
                return FALSE;
            }

            logger << "Отправка служебного кода для остановки службы ...";

            // Отправка кода SERVICE_CONTROL_STOP
            if (!ControlService(hDepService,
                                SERVICE_CONTROL_STOP,
                                (LPSERVICE_STATUS)&ssp))
            {
                // Освобождаем дескриптор зависимой службы
                CloseServiceHandle(hDepService);
                return FALSE;
            }

            logger << "Ожидаем остановку зависимой службы ...";

            // Цикл ожидания остановки службы
            while (ssp.dwCurrentState != SERVICE_STOPPED)
            {
                // Ожидаем столько времени, скольку указано в dwWaitHint зависимой службы
                Sleep(ssp.dwWaitHint);

                // Получение статуса зависимой службы
                if (!QueryServiceStatusEx(
                        hDepService,
                        SC_STATUS_PROCESS_INFO,
                        (LPBYTE)&ssp,
                        sizeof(SERVICE_STATUS_PROCESS),
                        &dwBytesNeeded))
                {
                    // Освобождаем дескриптор зависимой службы
                    CloseServiceHandle(hDepService);
                    return FALSE;
                }

                // Если служба уже остановлена - прекращаем ожидание
                if (ssp.dwCurrentState == SERVICE_STOPPED)
                {
                    break;
                }

                // В случае, если зависимая служба не изменила своего состояния в течении dwTimeout
                // выходим из данной функции
                if ((GetTickCount64() - dwStartTime) > dwTimeout)
                {
                    // Освобождаем дескриптор зависимой службы
                    CloseServiceHandle(hDepService);
                    return FALSE;
                }
            }

            // Освобождаем дескриптор зависимой службы
            CloseServiceHandle(hDepService);
        }

        // Освобождаем занятую память под буфер
        HeapFree(GetProcessHeap(), 0, lpDependencies);
    }

    return TRUE;
}

После того, как была объявлена и определена функция StopDependentServices пришла пора заняться основной функцией остановки нашей службы — DoStopSvc.

Для начала объявим её в файле WinService.h:

/* Остановка службы */
VOID __stdcall DoStopSvc(void);

Теперь разберём её пошаговое определение в файле WinService.cpp. Для начала, определим в функции основные переменные, которые в ней будут использоваться:

// Статус процесса службы
SERVICE_STATUS_PROCESS ssp;
ULONGLONG dwStartTime = GetTickCount64();
DWORD dwBytesNeeded;
ULONGLONG dwTimeout = 30000; 
DWORD dwWaitTime;

// Дескрипторы SCM и службы
SC_HANDLE schSCManager;
SC_HANDLE schService;

Далее получим дескриптор SCM и службы (заметьте, что при получении дескриптора службы мы указываем только интересующие нас флаги доступа и SERVICE_ENUMERATE_DEPENDENTS в их числе):

// Получение дескриптора SCM
schSCManager = OpenSCManager(
    NULL,                   
    NULL,                   
    SC_MANAGER_ALL_ACCESS);

if (NULL == schSCManager)
{
    logger << (LogMsg() << "OpenSCManager вернул NULL (" << GetLastError() << ")");
    return;
}

// Получение дескриптора службы
schService = OpenService(
    schSCManager, 
    SVCNAME,      
    SERVICE_STOP |
        SERVICE_QUERY_STATUS |
        SERVICE_ENUMERATE_DEPENDENTS);

if (schService == NULL)
{
    logger << (LogMsg() << "OpenService вернул NULL (" << GetLastError() << ")");
    CloseServiceHandle(schSCManager);
    return;
}

Теперь проверяем, запущена служба или остановлена с помощью уже известных функций:

// Убеждаемся, что служба ещё не остановлена
if (!QueryServiceStatusEx(
        schService,
        SC_STATUS_PROCESS_INFO,
        (LPBYTE)&ssp,
        sizeof(SERVICE_STATUS_PROCESS),
        &dwBytesNeeded))
{
    logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");

    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);
    return;
}

// Проверяем текущий статус службы (остановлена она или нет)
if (ssp.dwCurrentState == SERVICE_STOPPED)
{
    logger << "Служба уже остановлена";

    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);

    return;
}

Также необходимо обработать ситуацию, когда текущий статус нашей службы SERVICE_STOPPED_PENDING, ведь если служба уже останавливается, то нам необходимо лишь подождать её полной остановки и убедится что она точно остановлена:

// Ожидаем остановки службы, если она находится в состоянии остановки
while (ssp.dwCurrentState == SERVICE_STOP_PENDING)
{
    logger << "Служба останавливается ...";

    dwWaitTime = ssp.dwWaitHint / 10;
    if (dwWaitTime < 1000)
    {
        dwWaitTime = 1000;
    }
    else if (dwWaitTime > 10000)
    {
        dwWaitTime = 10000;
    }

    Sleep(dwWaitTime);

    // Получение текущего статуса службы
    if (!QueryServiceStatusEx(
            schService,
            SC_STATUS_PROCESS_INFO,
            (LPBYTE)&ssp,
            sizeof(SERVICE_STATUS_PROCESS),
            &dwBytesNeeded))
    {
        logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");

        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }

    if (ssp.dwCurrentState == SERVICE_STOPPED)
    {
        logger << "Служба успешно остановлена";

        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }

    if ((GetTickCount64() - dwStartTime) > dwTimeout)
    {
        logger << "Служба останавливалась слишком долго";

        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }
}

Далее необходимо остановить все зависимые службы и только после этого отправить запрос на остановку нашей службы через функцию ControlService:

// Останавливаем все зависимые службы
StopDependentServices(schSCManager, schService);

// Отправляем текущей службе запрос на остановку
if (!ControlService(
        schService,
        SERVICE_CONTROL_STOP,
        (LPSERVICE_STATUS)&ssp))
{
    logger << (LogMsg() << "ControlService вернул NULL (" << GetLastError() << ")");

    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);
    return;
}

И, наконец, дождёмся остановки нашей службы по уже знакомому ранее методу и завершим выполнение нашей функции:

// Ожидание завершения службы
while (ssp.dwCurrentState != SERVICE_STOPPED)
{
    Sleep(ssp.dwWaitHint);

    // Получение статуса службы
    if (!QueryServiceStatusEx(
            schService,
            SC_STATUS_PROCESS_INFO,
            (LPBYTE)&ssp,
            sizeof(SERVICE_STATUS_PROCESS),
            &dwBytesNeeded))
    {
        logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");

        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }

    if (ssp.dwCurrentState == SERVICE_STOPPED) {
        break;
    }

    if ((GetTickCount64() - dwStartTime) > dwTimeout)
    {
        logger << "Служба слишком долго завершалась";

        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }
}

logger << "Служба успешно остановлена";

CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);

Полный код функции DoSvcStop

/* Остановка службы */
VOID __stdcall DoStopSvc()
{
    // Статус процесса службы
    SERVICE_STATUS_PROCESS ssp;
    ULONGLONG dwStartTime = GetTickCount64();
    DWORD dwBytesNeeded;
    ULONGLONG dwTimeout = 30000; 
    DWORD dwWaitTime;

    // Дескрипторы SCM и службы
    SC_HANDLE schSCManager;
    SC_HANDLE schService;

    // Получение дескриптора SCM
    schSCManager = OpenSCManager(
        NULL,                   
        NULL,                   
        SC_MANAGER_ALL_ACCESS);

    if (NULL == schSCManager)
    {
        logger << (LogMsg() << "OpenSCManager вернул NULL (" << GetLastError() << ")");
        return;
    }

    // Получение дескриптора службы
    schService = OpenService(
        schSCManager, 
        SVCNAME,      
        SERVICE_STOP |
            SERVICE_QUERY_STATUS |
            SERVICE_ENUMERATE_DEPENDENTS);

    if (schService == NULL)
    {
        logger << (LogMsg() << "OpenService вернул NULL (" << GetLastError() << ")");
        CloseServiceHandle(schSCManager);
        return;
    }

    // Убеждаемся, что служба ещё не остановлена
    if (!QueryServiceStatusEx(
            schService,
            SC_STATUS_PROCESS_INFO,
            (LPBYTE)&ssp,
            sizeof(SERVICE_STATUS_PROCESS),
            &dwBytesNeeded))
    {
        logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");

        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }

    // Проверяем текущий статус службы (остановлена она или нет)
    if (ssp.dwCurrentState == SERVICE_STOPPED)
    {
        logger << "Служба уже остановлена";

        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);

        return;
    }

    // Ожидаем остановки службы, если она находится в состоянии остановки
    while (ssp.dwCurrentState == SERVICE_STOP_PENDING)
    {
        logger << "Служба останавливается ...";

        dwWaitTime = ssp.dwWaitHint / 10;
        if (dwWaitTime < 1000)
        {
            dwWaitTime = 1000;
        }
        else if (dwWaitTime > 10000)
        {
            dwWaitTime = 10000;
        }

        Sleep(dwWaitTime);

        // Получение текущего статуса службы
        if (!QueryServiceStatusEx(
                schService,
                SC_STATUS_PROCESS_INFO,
                (LPBYTE)&ssp,
                sizeof(SERVICE_STATUS_PROCESS),
                &dwBytesNeeded))
        {
            logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");

            CloseServiceHandle(schService);
            CloseServiceHandle(schSCManager);
            return;
        }

        if (ssp.dwCurrentState == SERVICE_STOPPED)
        {
            logger << "Служба успешно остановлена";

            CloseServiceHandle(schService);
            CloseServiceHandle(schSCManager);
            return;
        }

        if ((GetTickCount64() - dwStartTime) > dwTimeout)
        {
            logger << "Служба останавливалась слишком долго";

            CloseServiceHandle(schService);
            CloseServiceHandle(schSCManager);
            return;
        }
    }

    // Останавливаем все зависимые службы
    StopDependentServices(schSCManager, schService);

    // Отправляем текущей службе запрос на остановку
    if (!ControlService(
            schService,
            SERVICE_CONTROL_STOP,
            (LPSERVICE_STATUS)&ssp))
    {
        logger << (LogMsg() << "ControlService вернул NULL (" << GetLastError() << ")");

        CloseServiceHandle(schService);
        CloseServiceHandle(schSCManager);
        return;
    }

    // Ожидание завершения службы
    while (ssp.dwCurrentState != SERVICE_STOPPED)
    {
        Sleep(ssp.dwWaitHint);

        // Получение статуса службы
        if (!QueryServiceStatusEx(
                schService,
                SC_STATUS_PROCESS_INFO,
                (LPBYTE)&ssp,
                sizeof(SERVICE_STATUS_PROCESS),
                &dwBytesNeeded))
        {
            logger << (LogMsg() << "QueryServiceStatusEx вернул NULL (" << GetLastError() << ")");

            CloseServiceHandle(schService);
            CloseServiceHandle(schSCManager);
            return;
        }

        if (ssp.dwCurrentState == SERVICE_STOPPED) {
            break;
        }

        if ((GetTickCount64() - dwStartTime) > dwTimeout)
        {
            logger << "Служба слишком долго завершалась";

            CloseServiceHandle(schService);
            CloseServiceHandle(schSCManager);
            return;
        }
    }

    logger << "Служба успешно остановлена";

    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);
}

Теперь пришло время изменить точку входа в наше приложение и добавить в неё обработку аргумента /stop:

else if (lstrcmpi(argv[1], TEXT("/stop")) == 0)
{
    logger << "Запуск остановки службы ...";

    DoStopSvc();
    return 0;
}
else {
  // ...
}

Полный код точки входа в приложение

// Точка входа в приложение
int __cdecl _tmain(int argc, TCHAR *argv[])
{
    // Добавление поддержки русского языка в консоли
    setlocale(LC_ALL, "ru");

    if (lstrcmpi(argv[1], TEXT("/install")) == 0)
    {
        logger << "Запуск установки службы ...";

        // Вызов функции установки службы если был передан аргумент /install
        SvcInstall();
        return 0;
    }
    else if (lstrcmpi(argv[1], TEXT("/start")) == 0)
    {
        logger << "Запуск службы ...";

        DoStartSvc();
        return 0;
    }
    else if (lstrcmpi(argv[1], TEXT("/stop")) == 0)
    {
        logger << "Запуск остановки службы ...";

        DoStopSvc();
        return 0;
    }
    else
    {
        // Описание точки входа для SCM
        SERVICE_TABLE_ENTRY DispatchTable[] =
            {
                {WITHOUT_CONST(SVCNAME), (LPSERVICE_MAIN_FUNCTION)SvcMain},
                {NULL, NULL}};

        logger << "Запуск StartServiceCtrlDispatcher...";
        if (!StartServiceCtrlDispatcher(DispatchTable))
        {
            DWORD lastError = GetLastError();

            switch (lastError)
            {
            case ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:
            {
                logger << "ERROR_FAILED_SERVICE_CONTROLLER_CONNECT";
                break;
            }
            case ERROR_INVALID_DATA:
            {
                logger << "ERROR_INVALID_DATA";
                break;
            }
            case ERROR_SERVICE_ALREADY_RUNNING:
            {
                logger << "ERROR_SERVICE_ALREADY_RUNNING";
                break;
            }
            }
        }
    }

    return 0;
}

Теперь можно завершить выполнение нашей службы через запуск нашего приложения с аргументом /stop (перед этим службу лучше, конечно же, запустить):

dev-win-sc.exe /stop
Рисунок 19 - Служба успешно остановлена

Рисунок 19 — Служба успешно остановлена

В диспетчере задач служба также будет остановлена:

Рисунок 20 - Статус службы в диспетчере задач

Рисунок 20 — Статус службы в диспетчере задач

Кстати, если мы попробуем запустить наше приложение без аргументов, то вполне закономерно получим ошибку ERROR_FAILED_SERVICE_CONTROLLER_CONNECT, которая сообщает о том, что приложение запускается как консольное приложение, а не как служба:

Рисунок 21 - Ошибка запуска приложения

Рисунок 21 — Ошибка запуска приложения

Приложение как службу может запустить только SCM, стоит помнить об этом и не забывать.

Что ж, мы научились устанавливать службу в базу данных SCM, научились её запускать и останавливать, а теперь осталось лишь научится удалять службу из базы данных SCM или её деинсталлировать.

Реализация функции деинсталляции службы

Для начала объявим в файле WinService.h функцию DoDeleteSvc:

/* Деинстялляция службы */
VOID __stdcall DoDeleteSvc(void);

В файле WinService.cpp, как и с другими функциями, аналогично добавим её определение.

VOID __stdcall DoDeleteSvc(void) 
{
  // ...
}

Далее, определим дескриптор SCM и службы (обратите внимание, что указан флаг DELETE у прав доступа при открытии дескриптора службы):

// Дескриптор SCM и службы
SC_HANDLE schSCManager;
SC_HANDLE schService;

// Получение дескриптора SCM
schSCManager = OpenSCManager(
    NULL,                  
    NULL,                   
    SC_MANAGER_ALL_ACCESS);

if (NULL == schSCManager)
{
    logger << (LogMsg() << "OpenSCManager вернул NULL (" << GetLastError() << ")");
    return;
}

// Получение дескриптора службы
schService = OpenService(
    schSCManager, 
    SVCNAME,      
    DELETE);      // Права на удаление

if (schService == NULL)
{
    logger << (LogMsg() << "OpenService вернул NULL (" << GetLastError() << ")");

    CloseServiceHandle(schSCManager);
    return;
}

Теперь просто удаляем службу с помощью функции DeleteService и завершаем работу нашей функции:

// Удаление службы
if (!DeleteService(schService))
{
    logger << (LogMsg() << "DeleteService вернула NULL (" << GetLastError() << ")");
}
else
{
    logger << "Служба успешно удалена";
}

// Освобождение занятых дескрипторов
CloseServiceHandle(schService);
CloseServiceHandle(schSCManager);

Полный код функции DoDeleteSvc

/* Деинсталляция службы */
VOID __stdcall DoDeleteSvc()
{
    // Дескриптор SCM и службы
    SC_HANDLE schSCManager;
    SC_HANDLE schService;

    // Получение дескриптора SCM
    schSCManager = OpenSCManager(
        NULL,                  
        NULL,                   
        SC_MANAGER_ALL_ACCESS);

    if (NULL == schSCManager)
    {
        logger << (LogMsg() << "OpenSCManager вернул NULL (" << GetLastError() << ")");
        return;
    }

    // Получение дескриптора службы
    schService = OpenService(
        schSCManager, 
        SVCNAME,      
        DELETE);      // Права на удаление

    if (schService == NULL)
    {
        logger << (LogMsg() << "OpenService вернул NULL (" << GetLastError() << ")");

        CloseServiceHandle(schSCManager);
        return;
    }

    // Удаление службы
    if (!DeleteService(schService))
    {
        logger << (LogMsg() << "DeleteService вернула NULL (" << GetLastError() << ")");
    }
    else
    {
        logger << "Служба успешно удалена";
    }

    // Освобождение занятых дескрипторов
    CloseServiceHandle(schService);
    CloseServiceHandle(schSCManager);
}

Теперь снова видоизменим нашу точку входа в приложение добавив обработку аргумента /uninstall:

// ...
else if (lstrcmpi(argv[1], TEXT("/uninstall")) == 0)
{
    logger << "Запуск деинстялляции службы ...";

    DoDeleteSvc();
    return 0;
}
// ...

Полный код точки входа в приложение

// Точка входа в приложение
int __cdecl _tmain(int argc, TCHAR *argv[])
{
    // Добавление поддержки русского языка в консоли
    setlocale(LC_ALL, "ru");

    if (lstrcmpi(argv[1], TEXT("/install")) == 0)
    {
        logger << "Запуск установки службы ...";

        SvcInstall();
        return 0;
    }
    else if (lstrcmpi(argv[1], TEXT("/start")) == 0)
    {
        logger << "Запуск службы ...";

        DoStartSvc();
        return 0;
    }
    else if (lstrcmpi(argv[1], TEXT("/stop")) == 0)
    {
        logger << "Запуск остановки службы ...";

        DoStopSvc();
        return 0;
    }
    else if (lstrcmpi(argv[1], TEXT("/uninstall")) == 0)
    {
        logger << "Запуск деинсталляции службы ...";

        DoDeleteSvc();
        return 0;
    }
    else
    {
        // Описание точки входа для SCM
        SERVICE_TABLE_ENTRY DispatchTable[] =
            {
                {WITHOUT_CONST(SVCNAME), (LPSERVICE_MAIN_FUNCTION)SvcMain},
                {NULL, NULL}};

        logger << "Запуск StartServiceCtrlDispatcher...";
        if (!StartServiceCtrlDispatcher(DispatchTable))
        {
            DWORD lastError = GetLastError();

            switch (lastError)
            {
            case ERROR_FAILED_SERVICE_CONTROLLER_CONNECT:
            {
                logger << "ERROR_FAILED_SERVICE_CONTROLLER_CONNECT";
                break;
            }
            case ERROR_INVALID_DATA:
            {
                logger << "ERROR_INVALID_DATA";
                break;
            }
            case ERROR_SERVICE_ALREADY_RUNNING:
            {
                logger << "ERROR_SERVICE_ALREADY_RUNNING";
                break;
            }
            }
        }
    }

    return 0;
}

Теперь мы можем удалить (деинсталлировать) нашу службу из базы данных SCM:

Рисунок 22 - Деинсталляция службы

Рисунок 22 — Деинсталляция службы

Из диспетчера задач данная служба будет удалена, как и из окна «Службы». Чтобы снова запустить данную службу её сперва нужно установить.

Заключение

В рамках данной статьи было разработано приложение, которое управляет службой на Windows и позволяет выполнять в рамках этой службы какие-то полезные действия (например, запись в лог-файл значения счётчика).

С помощью разработанного приложения мы можем установить службу в базу данных SCM, запустить её, остановить её и все зависимые от неё службы, а также удалить (деинсталлировать) службу из SCM.

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

  1. /install — устанавливает службу в базу данных SCM, после чего её можно запустить или деинсталлировать;

  2. /start — запускает уже установленную службу из базы данных SCM. После запуска службу можно остановить или сразу деинсталлировать (не рекомендуется);

  3. /stop — останавливает уже запущенную службу. После запуска служба будет остановлена, если на момент запуска команды она была запущена;

  4. /uninstall — запускает процесс деинсталляции службы. После запуска служба будет удалена из базы данных SCM;

  5. Без аргументов — запускает точку входа в службу. Если запущена не SCM, то будет возвращена ошибка ERROR_FAILED_SERVICE_CONTROLLER_CONNECT, иначе будет выполнена точка входа в службу (функция SvcMain и SvcInit).

Исходный код начального и конечного проекта находятся в репозитории dev-win-service.

Список рекомендованной литературы

  1. Создание программы системного обслуживания Windows на C/C++ (более лаконичный программный код с ориентацией на классы, спасибо за ссылку @danilasar);

  2. Джонсон М. Харт. Системное программирование в среде Windows (спасибо за эту и следующие 4 ссылки @emusic);

  3. Дж. Рихтер, Дж. Кларк. «Программирование серверных приложений для Microsoft Windows 2000;

  4. Сергей Холодилов. Программирование служб: подробности;

  5. Александр Федотов. Управление системными службами Windows NT;

  6. Александр Фролов, Григорий Фролов. Программирование для Windows NT. Создание сервисного процесса.

Создание службы для Windows

Последнее обновление: 16.10.2019

Одним из важнейших компонентов ОС Windows являются службы. Фактически это отдельные приложения, которые не имеют графического интерфейса и
которые выполняют различные задачи в фоновом режиме. Службы могут быть запущены при старте операционной системы, так и в любой другой момент работы пользователя.
Распространенным примером служб являются различные веб-серверы, которые в фоновом режиме прослушивают
определенный порт на наличие подключений, и если подключения имеются, то взаимодействуют с ними. Это могут быть также различные вспомогательные сервисы
обновлений для других установленных программ, которые обращаются к серверу, чтобы узнать, есть ли новая версия приложения. В общем то мы можем
открыть панель служб и сами увидеть все установленные и запущенные службы:

Создание службы Windows в C#

Рассмотрим, как создавать свои службы в C#. В качестве реализуемой задачи выберем наблюдение за изменениями в определенной папке в файловой системе.
Теперь создадим для ее выполнения службу.

Вначале создадим новый проект, который будет иметь тип Windows Service. Назовем проект FileWatcherService:

Проект Windows Service в Visual Studio 2015

После этого Visual Studio генерирует проект, который имеет все необходимое. Хотя в принципе нам необязательно выбирать именно этот тип проекта,
можно было бы создать проект библиотеки классов, и затем в нем определить все необходимые классы.

Итак, новый проект выглядит следующим образом:

Здесь также есть файл Program.cs и есть собственно узел службы Service1.cs.

Служба представляет обычное приложение, но она не запускаетс сама по себе. Все вызовы и обращения к ней проходят через менеджер управления службами
(Service Control Manager или SCM). Когда служба запускается автоматически при старте системы или вручную, то SCM обращается к методу Main в классе Program:

static class Program
{
    static void Main()
    {
        ServiceBase[] ServicesToRun;
        ServicesToRun = new ServiceBase[]
        {
            new Service1()
        };
        ServiceBase.Run(ServicesToRun);
    }
}

Метод Main по умолчанию определен таким образом, чтобы запускать сразу несколько служб, которые определены в массиве ServicesToRun. Однако по умолчанию
проект содержит только одну службу Service1. Сам запуск производится с помощью метода Run: ServiceBase.Run(ServicesToRun).

Сама запускаемая служба представлена узлом Service1.cs. Однако на самом деле
это не простой файл кода. Если мы откроем этот узел, то увидим в нем файл дизайнера службы Service1.Designer.cs и класс Service1.

Класс Service1 собственно представляет службу. По умолчанию он имеет следующий код:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Diagnostics;
using System.Linq;
using System.ServiceProcess;
using System.Text;
using System.Threading.Tasks;

namespace FileWatcherService
{
    public partial class Service1 : ServiceBase
    {
        public Service1()
        {
            InitializeComponent();
        }

        protected override void OnStart(string[] args)
        {
        }

        protected override void OnStop()
        {
        }
    }
}

Класс службы должен наследоваться от базового класса ServiceBase. Этот класс определяет ряд методов, важнейшие из которых
метод OnStart(), который запускает действия, выпоняемые службой, и метод OnStop(), останавливающий
службу.

После того, как SCM вызовет метод Main и зарегистрирует службу, происходит непосредственный ее вызов через запуск метода OnStart.

Когда в консоли служб или через командную строку мы посылаем команду на остановку службы, то SCM обращается к методу OnStop для ее остановки.

Кроме этих двух методов в классе службы можно переопределить еще несколько методов базового класса ServiceBase:

  • OnPause: вызывается при приостановке службы

  • OnContinue: вызывается при возобновлении работы службы после ее приостановки

  • OnShutdown: вызывается при завершении работы Windows

  • OnPowerEvent: вызывается при изменении режима электропитания

  • OnCustomCommand: вызывается при получении службой пользовательской команды от Менеджера Управления Службами (Service Control Manager / SCM)

В конструкторе класса Service1 вызывается метод InitializeComponent(), который определен в файле дизайнера Service1.Designer.cs:

namespace FileWatcherService
{
    partial class Service1
    {
        private System.ComponentModel.IContainer components = null;

        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }
        private void InitializeComponent()
        {
            components = new System.ComponentModel.Container();
            this.ServiceName = "Service1";
        }
    }
}

Единственное, что надо в нем отметить, это установка названия службы (свойство ServiceName):

this.ServiceName = "Service1";

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

Теперь изменим код службы следующим образом:

using System;
using System.ServiceProcess;
using System.IO;
using System.Threading;

namespace FileWatcherService
{
    public partial class Service1 : ServiceBase
    {
        Logger logger;
        public Service1()
        {
            InitializeComponent();
            this.CanStop = true;
            this.CanPauseAndContinue = true;
            this.AutoLog = true;
        }

        protected override void OnStart(string[] args)
        {
            logger = new Logger();
            Thread loggerThread = new Thread(new ThreadStart(logger.Start));
            loggerThread.Start();
        }

        protected override void OnStop()
        {
            logger.Stop();
            Thread.Sleep(1000);
        }
    }

    class Logger
    {
        FileSystemWatcher watcher;
        object obj = new object();
        bool enabled = true;
        public Logger()
        {
            watcher = new FileSystemWatcher("D:\\Temp");
            watcher.Deleted += Watcher_Deleted;
            watcher.Created += Watcher_Created;
            watcher.Changed += Watcher_Changed;
            watcher.Renamed += Watcher_Renamed;
        }

        public void Start()
        {
            watcher.EnableRaisingEvents = true;
            while(enabled)
            {
                Thread.Sleep(1000);
            }
        }
        public void Stop()
        {
            watcher.EnableRaisingEvents = false;
            enabled = false;
        }
		// переименование файлов
        private void Watcher_Renamed(object sender, RenamedEventArgs e)
        {
            string fileEvent = "переименован в " + e.FullPath;
            string filePath = e.OldFullPath;
            RecordEntry(fileEvent, filePath);
        }
		// изменение файлов
        private void Watcher_Changed(object sender, FileSystemEventArgs e)
        {
            string fileEvent = "изменен";
            string filePath = e.FullPath;
            RecordEntry(fileEvent, filePath);
        }
		// создание файлов
        private void Watcher_Created(object sender, FileSystemEventArgs e)
        {
            string fileEvent = "создан";
            string filePath = e.FullPath;
            RecordEntry(fileEvent, filePath);
        }
		// удаление файлов
        private void Watcher_Deleted(object sender, FileSystemEventArgs e)
        {
            string fileEvent = "удален";
            string filePath = e.FullPath;
            RecordEntry(fileEvent, filePath);
        }

        private void RecordEntry(string fileEvent, string filePath)
        {
            lock (obj)
            {
                using (StreamWriter writer = new StreamWriter("D:\\templog.txt", true))
                {
                    writer.WriteLine(String.Format("{0} файл {1} был {2}", 
                        DateTime.Now.ToString("dd/MM/yyyy hh:mm:ss"), filePath, fileEvent));
                    writer.Flush();
                }
            }
        }
    }
}

Ключевым классом, который инкапсулирует всю функциональность, является класс Logger. С помощью объекта FileSystemWatcher он будет вести мониторинг изменений в папке D://Temp.
В методе Start() устанавливается, что мы будем отслеживать изменения через объект FileSystemWatcher. И вся работа будет идти, пока булевая переменная enabled равна true. А метод Stop() позволит
завершить работу класса.

События FileSystemWatcher позволяют отслеживать все изменения в наблюдаемой папке.
При этом будет вестись запись изменений в файл templog.txt. Чтобы не было гонки ресурсов
за файл templog.txt, в который вносятся записи об изменениях, процедура записи блокируется заглушкой lock(obj).

В итоге после создания, изменения, переименования и удаления файл лога будет содержать что-то наподобие:

30.07.2015 12:15:40 файл D:\Temp\Новый текстовый документ.txt был создан
30.07.2015 12:15:46 файл D:\Temp\Новый текстовый документ.txt был переименован в D:\Temp\hello.txt
30.07.2015 12:15:55 файл D:\Temp\hello.txt был изменен
30.07.2015 12:15:55 файл D:\Temp\hello.txt был изменен
30.07.2015 12:16:01 файл D:\Temp\hello.txt был удален

В самом классе службы Service1 в конструкторе устанавливается ряд опций:

this.CanStop = true; // службу можно остановить
this.CanPauseAndContinue = true; // службу можно приостановить и затем продолжить
this.AutoLog = true; // служба может вести запись в лог

В методе OnStart() для запуска объекта Logger вызывется новый поток:

protected override void OnStart(string[] args)
{
    logger = new Logger();
    Thread loggerThread = new Thread(new ThreadStart(logger.Start));
    loggerThread.Start();
}

Новый поток нужен, так как текущий поток обрабатывает только команды SCM и должен возвращаться из метода OnStart как можно быстрее.

Когда от менеджера SCM поступает команда на остановку службы, срабатывает метод OnStop, который вызывает метод logger.Stop(). Дополнительная задержка
позволит потоку логгера остановиться:

protected override void OnStop()
{
    logger.Stop();
    Thread.Sleep(1000);
}

Однако самого класса службы еще недостаточно. Нам необходимо еще создать устанощик службы.

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

В этой инструкции подробно о том, как создать службу в Windows 11 или Windows 10 средствами системы, ограничениях методов и одном дополнительном способе заставить работать ваш процесс в качестве службы.

Создание службы в командной строке

Первый способ — использование команды командной строки для создания своей службы, шаги будут следующими:

  1. Запустите командную строку от имени Администратора (способы запуска командной строки от Администратора).
  2. Используйте команду, заменив текстовые описания и пути на свои:
    sc create Имя_службы binPath="C:\service-file.exe" DisplayName= "Описание_службы" type=own start=auto
  3. После нажатия Enter вы получите сообщение: CreateService: успех, что означает, что служба была успешно создана.
    Создание службы в командной строке Windows

В указанной команде используются следующие параметры:

  • binPath — путь к исполняемому файлу службы.
  • DisplayName — отображаемое в списке служб имя службы.
  • start — тип запуска, возможные значения: boot, auto, demand (значение по умолчанию), disabled, delayed-auto
  • type — тип службы, по умолчанию own, возможны другие значения: share (делит исполняемый файл с другими службами), kernel (драйвер), filesys (драйвер файловой системы), interact (интерактивная служба с возможность взаимодействия с пользователем, поддержка этого типа служб прекращается).

После создания службы вы сможете увидеть её в списке служб (Win+Rservices.msc), а автоматический запуск произойдет при следующей перезагрузке системы.

Созданная служба в списке служб Windows

Создание службы в Windows PowerShell

Создать службу можно и в PowerShell, запущенном от имени администратора (или в Терминале Windows). Базовый вариант команды с параметрами по умолчанию:

New-Service -Name "Имя_Службы" -BinaryPathName '"C:\путь_к_файлу параметры_запуска"'

Расширенный вариант с указанием описания и типа запуска:

New-Service -Name MyService -BinaryPathName '"C:\путь_к_файлу параметры_запуска"' -DisplayName "Имя_службы" -Description "Описание службы" -StartupType "Automatic"
Создание службы в Windows PowerShell

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

-BinaryPathName C:\remontka.exe

При создании служб в PowerShell доступны и ряд дополнительных параметров, описанных в официальной справке на сайте Майкрософт.

Удаление созданной службы

Удалить созданную службы вы можете также в командной строке, запущенной от имени Администратора с помощью команды:

sc delete Имя_службы
Удаление службы в командной строке

Или в Windows PowerShell:

Remove-Service -Name MyService

После выполнения указанных команд созданная вами служба будет удалена из Windows.

Созданная служба не работает, варианты решения

Из описания можно предположить, что любой файл .exe может быть службой, однако, при попытке добавить свои фоновые утилиты и заставить их работать в виде службы рядовые пользователи обычно сталкиваются с ситуацией, когда процесс запускается, а затем закрывается сам. А при запуске службы вручную через некоторое время сообщает, что служба не ответила на запрос своевременно или о других ошибках.

Причина в том, что исполняемые файлы служб — не совсем обычные программы, они, помимо прочего, обмениваются данными с системой. Если ваш EXE не отвечает на соответствующие запросы системы, Windows «делает вывод» о том, что со службой что-то не так.

Как быть, если вы всё-таки настойчиво хотите использовать свой прикладной исполняемый файл в качестве службы?

  • Использовать планировщик заданий и запуск от соответствующего пользователя — это не будет в полной мере службой, но почти то, что нужно.
  • Ранее существовала программа RunAsSvc, позволяющая запускать любые процессы в качестве службы, но для современных ОC он не применима, а разработка была прекращена.
    Утилита RunAsSvc

  • Использовать инструменты INSTSRV.EXE и SRVANY.EXE из Windows Server 2003 Resource Kit Tools при создании службы Windows.

Создание пользовательской службы с помощью INSTSRV.EXE и SRVANY.EXE

Последний вариант из приведённого выше списка рассмотрим подробнее. Шаги будут следующими:

  1. Загрузите (вероятнее всего, придется найти в Интернете на сторонних сайтах) Windows Server 2003 Resource Kit полностью или только файлы INSTSRV.EXE и SRVANY.EXE, располагаем их у себя на диске, в моем примере путь — C:\Windows
  2. В командной строке от имени администратора используйте команду (пути меняем на свои):
    C:\Windows\instsrv.exe Имя_службы C:\Windows\srvany.exe
    Создание службы с помощью instsrv и srvany

  3. Если вы получили сообщение о том, что The service was successfully added, всё прошло успешно. Теперь требуется запустить редактор реестра (Win+Rregedit).
  4. В редакторе реестра перейдите по пути
    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\

    и найдите раздел с вашим именем службы. Нажимаем по нему правой кнопкой мыши и выбираем пункт «Создать» — «Раздел», задаем имя «Parameters» для раздела.

  5. Выберите созданный раздел, нажмите правой кнопкой мыши в правой панели редактора реестра и создайте новый строковый параметр с именем Application
  6. Дважды нажмите по параметру и в поле «Значение» укажите путь к вашему файлу exe для запуска службы.
    Путь к исполняемому файлу службы в реестре

Закройте редактор реестра — служба создана, её можно запустить из services.msc или она будет автоматически запущена после перезагрузки компьютера.

Учитывайте, что во всех приведенных примерах служба по умолчанию запускается с учетной записью «СИСТЕМА». В некоторых случаях это может приводить к неправильной работе. В этом случае может иметь смысл зайти в свойства службы в services.msc и изменить параметры на вкладке «Вход в систему».

In this tutorial, we will explore how to create a Windows Service with .NET Core. .NET Core is a versatile and powerful framework that allows developers to build a variety of applications, including Windows Services. The creation of Windows Services can be crucial for developing systems that require continuous background processes, such as system monitoring or data processing tasks.

How to Create a Windows Service with .NET Core image

Here’s the step-by-step guide to creating a Windows Service with .NET Core:

Step 1: Setting Up the .NET Core Console Application

First, you need to set up a new .NET Core console application. Open the command prompt and create a new directory for your project. Navigate into the directory and run the following command:

dotnet new console -n MyWindowsService

This command creates a new console application with the name ‘MyWindowsService’.

Step 2: Implementing the Main Functionality

Next, you need to implement the main functionality of your service. Open the ‘Program.cs’ file and add the following code:


using System;
using System.Threading;

namespace MyWindowsService
{
    class Program
    {
        static void Main(string[] args)
        {
            while(true)
            {
                Console.WriteLine("Windows Service is running...");
                Thread.Sleep(1000);
            }
        }
    }
}

This code will make the service print a message «Windows Service is running…» every second.

Step 3: Converting the Console App to a Windows Service

To convert the console app to a Windows Service, we will use the Microsoft.Extensions.Hosting package. First, add the package to your project with the following command:

dotnet add package Microsoft.Extensions.Hosting

Now, update the ‘Program.cs’ file:


using Microsoft.Extensions.Hosting;
using System;
using System.Threading;
using System.Threading.Tasks;

namespace MyWindowsService
{
    public class Program
    {
        public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

        public static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddHostedService<MyService>();
                });
    }

    public class MyService : IHostedService
    {
        public Task StartAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("Service starting...");
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("Service stopping...");
            return Task.CompletedTask;
        }
    }
}

This code will make your console application run as a Windows Service.

Step 4: Installing the Windows Service

To install the Windows Service, publish your application first:

dotnet publish --configuration Release --output ./publish

Then, install the service using the New-Service cmdlet from PowerShell:

New-Service -Name "MyService" -BinaryPathName "C:\path\to\your\app\publish\MyWindowsService.exe"

Your Windows Service is now installed and will start automatically with Windows.

If you’re looking to hire .NET remote developers to assist with similar projects, consider reaching out to experienced professionals.

Conclusion

Creating a Windows Service with .NET Core is a straightforward process that involves setting up a console application, implementing the main functionality, converting the app to a service, and installing the service. With this guide, you should be able to create your own Windows Services using .NET Core.

Remember, if you need professional help, don’t hesitate to hire .NET remote developers.

This article introduces Windows Services in .NET and how to create a Windows Service in C# and .NET using Visual Studio.

What is a Windows Service?

Windows Services are non-UI software applications that run in the background. Windows services usually start when an operating system boots and is scheduled to run in the background to execute some tasks. Windows services can also be started automatically or manually. You can also manually pause, stop and restart Windows services. 

Windows service is a computer program that runs in the background to execute some tasks. Some examples of Windows services are auto-update of Windows, check emails, print documents, SQL Server Agent, file and folder scanning and indexing, etc. If you open your Task Manager and click on the Services tab, you will see hundreds of services running on your machine. You can also see the statuses of these services. Some services are running, some have paused, and some have stopped. You can start, stop, and pause a service from here by right click on the service. 

You may also find all services running on your machine in the following ways:

  • Go to Control Panel and select «Services» inside «Administrative Tools.»
  • Next, open the Run window (Window + R), type services.msc, and press ENTER.

How to create a Windows service in C#?

Let’s create a Windows Service in C# using Visual Studio. 

Step 1

Open Visual Studio, click File > New, and select a project. Next, select a new project from the Dialog box, select «Window Service,» and click the OK button.

windows service

Step 2

Go to Visual C# ->» Windows Desktop» ->» Windows Service,» give an appropriate name and then click OK.

windows service

 

Once you click the OK button, the below screen will appear, which is your service.

windows service

 

Step 3

Right-click on the blank area and select «Add Installer.»

How to Add an Installer to a Windows Service

Before you can run a Windows Service, you need to install the Installer, which registers it with the Service Control Manager.

windows service

 

After Adding Installer, ProjectInstaller will add to your project, and ProjectInstakker.cs file will be open. Don’t forget to save everything (by pressing the ctrl + shift + s key)

windows service

 

Solution Explore looks like this:

 

Step 4

Right-click on the blank area and select «View Code»

windows service

Step 5

It has Constructor, which contains the InitializeComponent method:

The InitializeComponent method contains the logic which creates and initializes the user interface objects dragged on the forming surface and provides the Property Grid of Form Designer.

Very important: Don’t ever try to call any method before the call of the InitializeComponent process.

windows service

 

Step 6

Select the InitializeComponent method and press the F12 key to go definition.

windows service

 

Step 7

Now add the below line:

this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem;

You also can add a description and display the service name (optionally).

this.serviceInstaller1.Description = "My First Service demo";  
this.serviceInstaller1.DisplayName = "MyFirstService.Demo";  

windows service

Step 8

In this step, we will implement a timer and code to call the service at a given time. Then, we will create a text file and write the current time in the text file using the service. 

windows service

 

Service1.cs class

using System;  
using System.Collections.Generic;  
using System.ComponentModel;  
using System.Data;  
using System.Diagnostics;  
using System.IO;  
using System.Linq;  
using System.ServiceProcess;  
using System.Text;  
using System.Threading.Tasks;  
using System.Timers;  
namespace MyFirstService {  
    public partial class Service1: ServiceBase {  
        Timer timer = new Timer(); // name space(using System.Timers;)  
        public Service1() {  
            InitializeComponent();  
        }  
        protected override void OnStart(string[] args) {  
            WriteToFile("Service is started at " + DateTime.Now);  
            timer.Elapsed += new ElapsedEventHandler(OnElapsedTime);  
            timer.Interval = 5000; //number in milisecinds  
            timer.Enabled = true;  
        }  
        protected override void OnStop() {  
            WriteToFile("Service is stopped at " + DateTime.Now);  
        }  
        private void OnElapsedTime(object source, ElapsedEventArgs e) {  
            WriteToFile("Service is recall at " + DateTime.Now);  
        }  
        public void WriteToFile(string Message) {  
            string path = AppDomain.CurrentDomain.BaseDirectory + "\\Logs";  
            if (!Directory.Exists(path)) {  
                Directory.CreateDirectory(path);  
            }  
            string filepath = AppDomain.CurrentDomain.BaseDirectory + "\\Logs\\ServiceLog_" + DateTime.Now.Date.ToShortDateString().Replace('/', '_') + ".txt";  
            if (!File.Exists(filepath)) {  
                // Create a file to write to.   
                using(StreamWriter sw = File.CreateText(filepath)) {  
                    sw.WriteLine(Message);  
                }  
            } else {  
                using(StreamWriter sw = File.AppendText(filepath)) {  
                    sw.WriteLine(Message);  
                }  
            }  
        }  
    }  
}

Code explanation — the above code will call service every 5 seconds, create a folder if none exists, and write our message.

Step 9. Rebuild your application.

Right-click on your project or solution and select Rebuild.

windows service

 

Step 10

Search «Command Prompt» and run as administrator.

 

Step 11

Fire the below command in the command prompt and press ENTER.

cd C:\Windows\Microsoft.NET\Framework\v4.0.30319 

windows service

 

Step 12

Now Go to your project source folder > bin > Debug and copy the full path of your Windows Service exe file.

windows service

windows service

Installing a Windows Service

Open the command prompt and fire the below command and press ENTER.

Syntax

InstallUtil.exe + Your copied path + \your service name + .exe

Our path

InstallUtil.exe C:\Users\Faisal-Pathan\source\repos\MyFirstService\MyFirstService\bin\Debug\MyFirstService.exe

windows service

 

Check the status of a Windows Service.

Open services by following the below steps:

  1. Press the Window key + R.
  2. Type services.msc
  3. Find your Service.

windows service

You may notice that the Windows service is running. 

windows service

Check Windows Service Output 

The service will create a text file with the following text in it. 

windows service

 

The log folder will be created in your bin folder.

Uninstalling a Windows Service

If you want to uninstall your service, fire the below command.

  1. Syntax InstallUtil.exe -u + Your copied path + \your service name + .exe
  2. Our path InstallUtil.exe -u C:\Users\Faisal-Pathan\source\repos\MyFirstService\MyFirstService\bin\Debug\MyFirstService.exe

Summary

This article taught us how to create a Windows Service and install/Uninstall it using InstallUtil.exe from the command prompt.

I hope you found this tutorial easy to follow and understand. 

I also uploaded this project on GitHub; here is the URL https://github.com/faisal5170/WindowsService.

Понравилась статья? Поделить с друзьями:
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
  • Очистить диск с windows vista от ненужных файлов
  • Windows defender как отключить windows 10 командная строка
  • Windows 10 english iso torrent
  • Установка windows на андроид 4pda
  • Имя пользователя windows не должно содержать кириллицу