В данной статье описан пошаговый процесс разработки служб для операционной системы Windows с использованием языка программирования C++.
В статье будет представлена теоретическая база по работе служб в Windows, рассмотрено их общее устройство и будет реализовано приложение, с помощью которого можно будет устанавливать свою службу, запускать её, останавливать и удалять (деинсталлировать).
Содержание
Введение
Не так давно я начал большую часть своего рабочего времени посвящать программированию на языке C++ и в ходе решения практических задач возникла потребность написать свою службу для операционной системы Windows.
В результате поиска справочных материалов, примеров реализации служб и литературы для полноценного понимания их разработки я столкнулся с проблемой разрозненности информации в разных источниках. Узнать как полноценно разработать службу на Windows используя только один источник - сложная задача. Даже в официальной документации Майкрософт все примеры разрознены и разбросаны по разным страницам и чтобы снабдить своё приложение основными функциями приходится искать и соединять разные блоки кода из разных примеров. Кроме того исходный код примеров не лишён недостатков. Например, используя код из официальной документации можно столкнуться с проблемой отсутствия обратной связи в работе службы, потому что примеры не содержат средства для вывода сообщений о ходе работы службы, из-за чего изучение данной темы может вызвать трудности у тех, кто с этой темой только начинает работу.
Для упрощения разработки служб я подготовил начальный проект с уже встроенным простым механизмом логирования сообщений в файл, который может работать со множеством потоков и помогает получать информацию о ходе работы службы в фоновом режиме. В исходном коде приведены примеры использования данного логгера.
В рамках статьи я буду пошагово объяснять как разработать службу Windows на C++ и в результате проделанной работы получится приложение, которое умеет устанавливать службу в базу данных диспетчера управления службами, запускать эту службу, останавливать и деинсталлировать её. В общем, будет дано руководство, по которому можно разработать основные функции управления службой.
Ссылка на исходный код
Исходный код начального и конечного проекта находятся в репозитории dev-win-service.
Что такое служба?
Прежде чем мы начнём разрабатывать службу необходимо освоить теоретический минимум, который нужен для полноценного понимания устройства таких приложений.
Служба Windows (Windows Service) - это приложение, которое работает в операционной системе Windows в фоновом режиме, независимо от действий пользователя (если они не направлены на остановку службы).
Службы могут запускаться автоматически при загрузке компьютера, их можно запускать, останавливать, перезапускать и удалять. Они отлично подходят для использования на сервере, а также в ситуациях, когда необходимо запустить долгосрочные процессы в фоновом режиме, чтобы не мешать работе пользователей, но при этом выполнять полезную работу.
Важно заметить, что сами по себе службы как приложения не имеют пользовательского интерфейса, а потому чтобы увидеть запущенную службу необходимо обратиться к диспетчеру задач и вкладке "Службы". Именно в данной вкладке можно отслеживать текущее состояние службы.
В диспетчере задач есть множество столбцов, каждый из которых даёт информацию о службе. На рисунке 1 этих столбцов 4: имя, ИД (идентификатор) процесса, описание и состояние (если сделать скролл вправо будет ещё один столбец - группа).
Идентификатор процесса службы определяет основной процесс приложения в операционной системе, которое было запущено как служба. Чтобы посмотреть подробное описание процесса приложения, запущенного как служба, можно выполнить следующую команду в программной оболочке PowerShell:
Get-Process -Id <PID>
Можно также посмотреть информацию о идентификаторе процесса службы, методе запуска, состоянии, статусе и возвращаемом коде через PowerShell скрипт по имени службы:
$service = Get-WmiObject -Class Win32_Service -Filter "Name='<NAME SERVICE>'"
$service
Хоть службы и лишены пользовательского интерфейса, но они могут пригодится при работе с этим самым пользовательским интерфейсом. Служба выполняет роль фонового процесса, а потому может совершенно спокойно взаимодействовать с каким-нибудь клиентом на Qt через сокеты или другой программный интерфейс выполняя роль "сервера" для, например, безопасного выхода в сеть, взаимодействия с другими удалёнными серверами и многого другого. Если обобщить, служба идеально подходит для выполнения фоновых задач оставляя на клиенте задачи, которые связанны с отрисовкой пользовательского интерфейса.
Стоит также отметить, что в диспетчере задач информация о службах представлена в краткой форме и для более детального ознакомления со списком всех служб можно открыть специальной окно "Службы" (которое доступно из диспетчера задач). Выглядит оно следующим образом:
В этом окне можно выбрать отдельные службы, получить более детальную о них информацию и посмотреть полное описание служб (ведь в диспетчере задач находится только краткое описание).
Время существования службы
Служба преодолевает множество внутренних состояний за время своего существования.
С самого начала служба должна быть установлена в системе, в которой она будет выполняться. Это необходимо для того, чтобы загрузить её в диспетчер управления службами (Service Control Manager - SCM), для дальнейшей с ней работы.
SCM - это основное средство для управления службами в Windows, которое запускается при загрузке операционной системы и работает со специальной базой данных служб.
Установка службы (как и её удаление) происходит с помощью сценариев *.INF и SetupAPI. После установки службы она загружается в базу данных служб и через SCM с ней можно будет полноценно работать. Запустить службу можно сразу же после её установки, а уже запущенную службу можно остановить перед её удалением (или деинсталляцией).
Теперь разберём основные состояния службы:
SERVICE_STOPPED - служба остановлена;
SERVICE_RUNNING - служба выполняется;
SERVICE_PAUSED - служба приостановлена (на паузе);
SERVICE_STOP_PENDING - служба находится в процессе остановки;
SERVICE_START_PENDING - служба находится в процессе запуска;
SERVICE_PAUSED_PENDING - служба находится в процессе приостановки (постановки на паузу);
SERVICE_CONTINUE_PENDING - служба находится в процессе возобновления после приостановки.
Все состояния службы напрямую транслируются в диспетчер задач, некоторые из них увидеть трудно (в основном - все состояния с постфиксом _PENDING), т.к. состояния могут очень быстро обновится (можно пойти на хитрость и задать задержку между операциями запуска, остановки или перезапуска, чтобы визуализировать все возможные состояния, но в данной статье это рассмотрено не будет), однако основные состояния: остановки, выполнения и приостановки увидеть можно и это прекрасно отражает столбец "Состояние" в диспетчере задач.
Подготовительный этап
Прежде чем приступить к практической части статьи и успешно воспроизвести все примеры необходимо скачать исходники программного кода с минимальным набором функций, которые потребуются на начальном этапе, а именно: готовой конфигурацией CMake и логгером, с помощью которого можно добавлять записи в файл.
Логгер особенно необходим, поскольку как мы ранее выяснили службы не имеют пользовательского интерфейса, однако они имеют доступ к файловой системе, а значит можно мониторить их работу с помощью записи в файл. Рассматривать особенности разработки логгера в данной статье мы не будем, но важно отметить, что наш логгер умеет контролировать доступ к одному файлу несколькими потоками через мьютекс, а потому мы можем быть спокойны при его использовании в разных потоках.
Загрузить начальный проект можно по этой ссылке.
После загрузки начального проекта и его запуска (необходимо убедится, что конфигурация CMake удовлетворяет вашим текущим параметрам системы) результат выполнения программы будет следующий:
По умолчанию в CMake значение флага DEBUG установлено в 1, чтобы выводить сообщения логгера прямо в консоль.
Это можно исправить изменив соответствующее значение на 0 в параметрах конфигурации CMake.
После выполнения кода в каталоге проекта должна быть создана директория, в которую будут складироваться логи за текущую дату (в формате YYYY-MM-DD). В моём случае это каталог 20241108. Содержимое файла лога можно увидеть ниже:
Убедившись, что всё работает, можно приступать к следующему шагу - разработке службы 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++.
Основные особенности данного соглашения:
Аргументы функций передаются через стек, справа налево;
Аргументы, размер которых меньше 4 байт, расширяются до 4 байт;
За сохранение регистров EAX, ECX, EDX и стека сопроцессора отвечает вызывающая программа, за остальные - вызываемая функция;
Очистку стека производит вызывающая программа.
О функции _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 поменять, то путь до исполняемого файла может быть другим.
После установки службы в диспетчере задач и окне "Службы" её может быть не видно сразу, поэтому может сложиться впечатление что установка на самом деле не удалась, однако после перезагрузки этих окон данная служба появится:
На рисунке 11 можно отметить, что настройки, применяемые при создании службы, были отражены в окне "Службы". Поскольку мы устанавливали стартовое имя (вход по имени) как LocalSystem, а тип запуска как SERVICE_DEMAND_START (Вручную).
Также, если мы теперь посмотрим более детальную информацию об этой службе через PowerShell, то можем обнаружить интересную информацию:
ExitCode 1077 службы в Windows означает, что с момента последней загрузки службы попытки её запустить не предпринимались. То есть она была загружена, но не была запущена. Такое справедливо для всех служб в Windows:
Что ж, самое время научится запускать службу, чтобы она выполняла какую-нибудь полезную нагрузку.
Реализация функции запуска службы
Начнём с добавления объявления функции DoStartSvc в заголовочный файл WinService.h:
// Запуск службы
VOID __stdcall DoStartSvc(void);
__stdcall - это специальное соглашение о вызовах, которое применяется в ОС Windows для вызова функций WinAPI.
Особенности данного соглашения следующие:
Аргументы функций передаются через стек, справа налево;
Очистку стека производит вызываемая программа;
Возвращаемое значение записывается в регистр 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 для вызывающего процесса.
Теперь протестируем наши новые добавленные функции (если вы ещё не выполнили команду /install, то необходимо выполнить данный шаг, иначе вы получите ошибку 1073 - служба уже установлена).
Для запуска службы выполняем следующую команду в консоли:
dev-win-sc.exe /start
После запуска службы в диспетчере задач можно увидеть, что ей был выдан конкретный идентификатор процесса и её состояние изменилось на "Выполняется" с "Остановлено":
Через PowerShell мы также можем посмотреть изменённое состояние службы:
Сейчас службу можно остановить только через диспетчер задач.
В лог-файле мы можем увидеть следующую картину (я намерено остановил службу чтобы посмотреть и эту запись в логе):
Как видим служба успешно завершила свою работу. В диспетчере задач у неё будет состояние "Остановлено".
В функции 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());
// ...
}
Теперь запустим службу, немного подождём и отключим её через диспетчер задач. В лог-файл должно записаться примерно следующее:
Функция 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
В диспетчере задач служба также будет остановлена:
Кстати, если мы попробуем запустить наше приложение без аргументов, то вполне закономерно получим ошибку ERROR_FAILED_SERVICE_CONTROLLER_CONNECT, которая сообщает о том, что приложение запускается как консольное приложение, а не как служба:
Приложение как службу может запустить только 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:
Из диспетчера задач данная служба будет удалена, как и из окна "Службы". Чтобы снова запустить данную службу её сперва нужно установить.
Заключение
В рамках данной статьи было разработано приложение, которое управляет службой на Windows и позволяет выполнять в рамках этой службы какие-то полезные действия (например, запись в лог-файл значения счётчика).
С помощью разработанного приложения мы можем установить службу в базу данных SCM, запустить её, остановить её и все зависимые от неё службы, а также удалить (деинсталлировать) службу из SCM.
Опишем аргументы приложения, которые используются для управления службой:
/install - устанавливает службу в базу данных SCM, после чего её можно запустить или деинсталлировать;
/start - запускает уже установленную службу из базы данных SCM. После запуска службу можно остановить или сразу деинсталлировать (не рекомендуется);
/stop - останавливает уже запущенную службу. После запуска служба будет остановлена, если на момент запуска команды она была запущена;
/uninstall - запускает процесс деинсталляции службы. После запуска служба будет удалена из базы данных SCM;
Без аргументов - запускает точку входа в службу. Если запущена не SCM, то будет возвращена ошибка ERROR_FAILED_SERVICE_CONTROLLER_CONNECT, иначе будет выполнена точка входа в службу (функция SvcMain и SvcInit).
Исходный код начального и конечного проекта находятся в репозитории dev-win-service.
Комментарии (60)
Vad344
09.11.2024 17:59std::cout
...а ведь и прпвда c++ код. ;)
PS: статья понравилась, все нужное собрано в одном месте.
emusic
09.11.2024 17:59а ведь и прпвда c++ код
В том смысле, что компилятор C его не примет - безусловно. :)
все нужное собрано в одном месте
Я уже считать устал, сколько раз оно так в одном месте собиралось за прошедшие десятки лет...
dan_sw Автор
09.11.2024 17:59Я уже считать устал, сколько раз оно так в одном месте собиралось за прошедшие десятки лет...
Не могли бы Вы поделится ссылкой (или ссылками), где аналогичный материал собран в одном конкретном месте и где так же (или даже лучше) рассматривается подобная тема? Не исключаю, что я, возможно, плохо искал и поэтому столкнулся с проблемой разрозненности информации по разным источникам.
Думаю не только мне будет полезно ознакомится с другими работами, где данная тема широко освящена.
emusic
09.11.2024 17:59Основное "конкретное место", как тут уже отметили - MSDN. Там есть абсолютно все, что может потребоваться для создания службы. А чрезмерно подробное разжевывание в стиле "для начинающих" - классическая медвежья услуга. Не должны начинающие заниматься такими вещами, никогда.
О программировании служб Win32 очень много написано в конце 90-х и начале 2000-х, сейчас ссылки на ту литературу навскидку не найти - они завалены тоннами хлама, сгенерированного ИИ. Вот что попалось среди хлама:
Джонсон М. Харт. Системное программирование в среде Windows.
Дж. Рихтер, Дж. Кларк. «Программирование серверных приложений для Microsoft Windows 2000
Александр Федотов. Управление системными службами Windows NT.
Сергей Холодилов. Программирование служб: подробности
Всеволод Стахов. Программирование сервисов в Windows 2000
Александр Фролов, Григорий Фролов. Программирование для Windows NT. Создание сервисного процесса
А уж сколько понаписано про создание служб на C# - в глазах рябит.
dan_sw Автор
09.11.2024 17:59Спасибо за то, что уделили время поиску данных источников, думаю что они действительно могут расширить общее представление о том, как эти службы разрабатывать)
Однако представленные источники (кроме двух) не описывают полностью процесс разработки служб, а лишь рассказывают о некоторых их особенностях (где-то есть только установка и деинсталляция, а где-то и этого нет). Материал из данных источников довольно устаревший (есть ссылка где публикация была аж в 1996 году), и какие-то концепции могут быть подвержены критике (что-то может уже не использоваться).
Более близкий материал по содержанию есть по Вами приведённым ссылкам (там есть всё, о чём я писал и даже больше):
Александр Федотов. Управление системными службами Windows NT.
Джонсон М. Харт. Системное программирование в среде Windows.
Это действительно бесценный материал, который стоит изучить. Конечно Windows NT (2005 года) это не Windows 10 (на которой я службу разрабатывал) и не Windows 11, но, думаю, есть какие-то общие детали, которые и там и тут используются. В книге по системному программированию так вообще всего предостаточно)
В любом случае нужно периодически актуализировать информацию со старых источников, не даром книги переиздаются и корректируются (особенно технические).
emusic
09.11.2024 17:59Материал из данных источников довольно устаревший
"Довольно устаревшие" - это Ваши тексты якобы на C++ (понятное дело - для кликбейта), а на самом деле - очень топорно переделанные с каких-то старых текстов на C. :) Стыдно должно быть за такое.
Конечно Windows NT (2005 года) это не Windows 10 (на которой я службу разрабатывал) и не Windows 11
И что же в Windows 10/11 появилось нового в отношении служб? Последнее сколько-нибудь значимое нововведение было в Vista (тэги и запуск системных служб в нескольких процессах), но для разработки собственной службы это не актуально.
dan_sw Автор
09.11.2024 17:59"Довольно устаревшие" - это Ваши тексты якобы на C++ (понятное дело - для кликбейта)
Никакой цели добавить в текст статьи "кликбейт" я не преследовал. То, что я использую именно C++, а не Си можно понять банально по расширению файлов исходного кода: там *.cpp используется, а не *.hpp.
Если я использую инструкции, которые можно на Си написать, это не значит что я использую везде Си.
Стыдно должно быть за такое
Почему стыдно? И почему должно так быть? Не понимаю. Материал я написал разобравшись в этой теме настолько, сколько необходимо для написания своей службы на C++ для решения прикладной задачи. Изученный мной материал я подробно расписал в виде туториала и выложил его на данную платформу. Писал я его для себя и для читателей, которым интересна данная тема. Хорошо бы если через пару лет вернувшись к этому материалу и я и читатель отлично понимал как разработать свою службу на Windows с использованием C++ и, может быть, щепоткой Си. Не знаю, за что мне должно быть стыдно.
И что же в Windows 10/11 появилось нового в отношении служб?
Ну за 20 лет точно что-то новое появилось) Будьте уверены) Что именно - я не знаю, но учитывая что кодовая база Windows растёт, то и службы (API для их работы и управления) эта кодовая база точно задевает)
Где-то оптимизацию поправили, где-то новое API добавили, где-то поток по другому работает или раздробили один на два. Ну, в общем точно что-то поменялось. На этот счёт можно другую статью написать о том, как изменилась работа служб в Windows) Я лишь исхожу из того, что развитие ОС Windows во времени предполагает изменения её API и подходов для разработки служб (как пример).
Считать, что ничего не изменилось - тоже не корректно. Известно же что в ИТ всё очень быстро развивается. А если это не так - нужны пруфы.
emusic
09.11.2024 17:59То, что я использую именно C++, а не Си можно понять банально по расширению файлов исходного кода
То есть, если я помещу программу на чистом C в файл с расширением .cpp, она автоматически станет "программой на C++"?
там *.cpp используется, а не *.hpp
Вы уже второй раз откровенно палитесь с этим ".hpp". Для справки: .hpp - это типовое расширение заголовка на C++, содержащего не только объявления/определения, но и реализацию (или бОльшую ее часть). Утверждению "на C++, а не на Си" соответствовало бы ".cpp, а не .c".
Если я использую инструкции, которые можно на Си написать
Вы их используете преимущественно. А еще создается впечатление, что Вы толком не знаете ни C, ни C++.
Материал я написал разобравшись в этой теме настолько, сколько необходимо для написания своей службы на C++
Да, Вы написали его в стиле "как значки и строчки объединить в группы, чтобы получилась работающая служба". Статьи в таком стиле неплохо смотрятся под названиями "использование GDI для рисования фигур", "ваша первая многопоточная программа" и т.п. Но в сфере системного программирования это выглядит довольно убого.
Увлекшись разжевыванием деталей, очевидных для любого мало-мальски опытного программиста, Вы даже ни словом не упомянули того, что служба по умолчанию запускается от имени системы (LOCAL_SYSTEM), а это еще серьезнее, чем права администратора.
и я и читатель отлично понимал как разработать свою службу на Windows с использованием C++ и, может быть, щепоткой Си
Для начала неплохо бы понимать, из каких соображений выбран C++, для чего там может потребоваться (или не потребоваться) "щепотка Си", и чем эти языки отличаются. У Вас, судя по всему, это понимание весьма поверхностное.
Ну за 20 лет точно что-то новое появилось)
А конкретно?
Будьте уверены)
Отличный аргумент, непробиваемый.
Где-то оптимизацию поправили, где-то новое API добавили, где-то поток по другому работает или раздробили один на два. Ну, в общем точно что-то поменялось
Если Вы о том, как организованы стандартные системные службы, то там как раз много чего регулярно меняется. Но Ваша статья не об этом.
исхожу из того, что развитие ОС Windows во времени предполагает изменения её API и подходов для разработки служб (как пример)
Так в чем же конкретно поменялись API и/или подходы к разработке?
К чему у Вас, например, вот это: "Выделение буфера для зависимых служб происходит с помощью функции HeapAlloc"
Почему HeapAlloc, а не new, malloc, LocalAlloc, GlobalAlloc?
А это: " нужно учитывать, что выделенная память не может быть куда-либо перемещена"?
Кем перемещена, когда, зачем?
А какой смысл читатель Вашей статьи должен извлечь из этой фразы: "Функция StartServiceCtrlDispatcher соединяет основной поток процесса службы с SCM, в результате чего этот поток становится потоком SCM для вызывающего процесса"?
Что значит "соединяет поток"? Что такое "поток SCM для вызывающего процесса"?
dan_sw Автор
09.11.2024 17:59Вы уже второй раз откровенно палитесь с этим ".hpp"
Да, это действительно моя ошибка. Я почему-то про этот *.hpp формат принял ошибочно за *.c, действительно ошибся.
Заголовки для Си указываются конечно же в файле с расширением *.h, а исходники с расширением *.c. Для C++ исходники с расширением *.cpp, а заголовки можно как *.h или *.hpp оформлять. Тут Вы верно подметили.
Однако я не "палюсь", это лишь ошибка которую я в комментариях уже никак поправить не могу. Чтобы "палится" нужно сначала кем-то "казаться" или формировать образ о себе, всем о нём говорить и не подтверждать его своими действиями или словами. С тем же успехом можно утверждать, что Вы "палитесь" что не HTML-верстальщик, ведь HTML-верстальщики так не пишут :)
Увлекшись разжевыванием деталей, очевидных для любого мало-мальски опытного программиста, Вы даже ни словом не упомянули того, что служба по умолчанию запускается от имени системы (LOCAL_SYSTEM), а это еще серьезнее, чем права администратора.
Как это не упоминаю? Я это чётко обозначил в программном коде:
// Создание службы и получение её дескриптора 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); // Пароль
Если бы я расписывал все параметры функции CreateServiceA, то статья выросла бы ещё больше)
А какой смысл читатель Вашей статьи должен извлечь из этой фразы: "Функция StartServiceCtrlDispatcher соединяет основной поток процесса службы с SCM, в результате чего этот поток становится потоком SCM для вызывающего процесса"?
Что значит "соединяет поток"? Что такое "поток SCM для вызывающего процесса"?
Согласен, могут возникнуть трудности в понимании что тут я сформулировал. Думаю подкорректирую этот текст позже, чтобы не было недопониманий
emusic
09.11.2024 17:59Я почему-то про этот *.hpp формат принял ошибочно за *.c, действительно ошибся
Фишка в том, что мало-мальски опытный программист таких ляпов не допустит, это где-то на уровне подкорки.
Заголовки для Си указываются конечно же в файле с расширением *.h, а исходники с расширением *.c. Для C++ исходники с расширением *.cpp, а заголовки можно как *.h или *.hpp оформлять.
И таких пространных объяснений мало-мальски опытный программист тоже давать не будет. Это порождает сомнения в том, что Вы сколько-нибудь хорошо знаете что C, что C++, и что у Вас есть мало-мальски приличный опыт их использования.
Чтобы "палится" нужно сначала кем-то "казаться"
Опубликовав статью на тему системного программирования, без предисловия в стиле "я вообще-то токарь, но вот почитал того-сего, и решил свести воедино, больно не бейте, если налажал", Вы заявили о себе, как о квалифицированном специалисте. Допуская в комментариях откровенные ляпы, Вы и палитесь.
Как это не упоминаю? Я это чётко обозначил в программном коде
Вы действительно полагаете, что типичный дилетант в системном программировании, на которого ориентирована Ваша статья, даже не то, чтобы поймет, а вообще заметит эту ремарку?
Блин, я ведь сперва как-то проглядел, что у Вас там написано до "(LocalSystem)". "Стартовое имя" - это что вообще? Вы точно уверены, что понимаете смысл этого параметра?
Согласен, могут возникнуть трудности в понимании что тут я сформулировал
Беда в том, что Вы этого не понимаете (как и ряда других вещей), однако берете на себя функцию обучающего, а не обучаемого.
dan_sw Автор
09.11.2024 17:59Фишка в том, что мало-мальски опытный программист таких ляпов не допустит, это где-то на уровне подкорки.
Ну, это Ваше мнение, как и всё прочее. Вы знаете как работает мозг на уровне "подкорки"? Расскажите об этом. Интересны Ваши знания в области нейрохирургии) Вдруг Вы бывший нейрохирург и отлично знаете как работает мозг программиста? Всё может быть)
И таких пространных объяснений мало-мальски опытный программист тоже давать не будет. Это порождает сомнения в том, что Вы сколько-нибудь хорошо знаете что C, что C++, и что у Вас есть мало-мальски приличный опыт их использования.
Вообще, поделюсь своим опытом использования C++ (не смотря на Ваше субъективное мнение он у меня есть): с данным языком программирования я знаком ещё со школы. В школьные годы писал свою текстовую базу данных, простые приложения на WinAPI (преимущественно связанные с графикой), пробовал писать компилятор под Pascal и сделал дюжину консольных игр (для обычной консоли, терминала).
После школы учился в университете, где C++ использовал, к сожалению, крайне мало. В основном приложения на Qt, какие-то лабораторки, ну а затем и вовсе перешёл на Java/C#/JavaScript/Python, а о нём вспомнил только пару месяцев назад, когда потребовалось разобраться в вычислениях на CUDA. Ну и по работе появились задачи, которые с этим языком необходимо решать, а опыт его использования у меня есть.
Вообще этот язык мне очень импонирует и я с ним, можно сказать, мечтал "серьёзно" поработать. В общем-то сейчас я с ним и работаю "серьёзно"). Не зря же я читал кучу литературы об этом языке в своё время) А так я больше программировал на JavaScript/TypeScript последние пару лет.
Если программист успешно работает с каким-то одним языком программирования, то перейти на другой язык программирования (тем более с которым он работал ранее) будет не так трудно. Ведь программист уже не новичок и понимает как строятся программы. Я не новичок, в программировании уже довольно давно и это моё дело. Я его выбрал, я им занимаюсь, я в нём со временем расту.
По тексту комментариев или статьи очень сложно понять какой у программиста уровень, потому что словесное изложение мыслей это не тоже самое, что извергать код на интуитивно понятном уровне, который решает определённым образом задачу через какой-то алгоритм. Не знаю, как Вы этот уровень оцениваете.
Опубликовав статью на тему системного программирования, без предисловия в стиле "я вообще-то токарь, но вот почитал того-сего, и решил свести воедино, больно не бейте, если налажал", Вы заявили о себе, как о квалифицированном специалисте. Допуская в комментариях откровенные ляпы, Вы и палитесь.
Да, я квалифицированный специалист из области программной инженерии. Я не палюсь, а допускаю ошибки - это нормально. Даже если эти ошибки "откровенные ошибки". И я не стану писать "больно не бейте, если налажал" чтобы получить какого-то "снисхождения" (думаю Вы это имели ввиду) потому что если налажал - исправлю, если будут "бить" могу "стукнуть" в ответ. Всё довольно просто.
Вы действительно полагаете, что типичный дилетант в системном программировании, на которого ориентирована Ваша статья, даже не то, чтобы поймет, а вообще заметит эту ремарку?
Моя статья ориентирована не на дилетантов, а на тех, кто более менее уже разбирается в программировании на C++ :) Посмотрите на уровень сложности статьи. Там стоит - "Сложный". Содержание соответствует уровню.
Беда в том, что Вы этого не понимаете (как и ряда других вещей), однако берете на себя функцию обучающего, а не обучаемого.
Я допускаю, что у меня есть пробелы в знаниях, но при выявлении этих пробелов я их успешно ликвидирую со временем. Я писал статью не только для того, чтобы предоставить материал по которому можно службы написать, но ещё и сам повторял уже изученные элементы и укреплял о них своё понимание. Так что я и обучающийся, и обучающий. Для кого как. Всё субъективно.
ViacheslavNk
09.11.2024 17:59Никакой цели добавить в текст статьи "кликбейт" я не преследовал. То, что я использую именно C++, а не Си можно понять банально по расширению файлов исходного кода: там *.cpp используется, а не *.hpp.
Ну почестному С++ там как раз не много, как минимум напрашивается класс Service, так же для хенделов какой-нибудь класс Handle с использованием RAII.
Плюс не рассмотрены кейсы когда требуются ретраии и случаи когда функции SCM зависают и нужно делать асинхронные вызовы.
dan_sw Автор
09.11.2024 17:59Плюс не рассмотрены кейсы когда требуются ретраии и случаи когда функции SCM зависают и нужно делать асинхронные вызовы.
Не могли бы Вы подробнее описать данную проблему, которая может возникнуть? Хотелось бы её подробнее изучить. Если скинете ещё и ссылки на материал - будет вообще замечательно) Не совсем просто понимаю, когда функции SCM могут зависнуть и что имеется ввиду под "асинхронными вызовами" (понятно что это такое, но в контексте SCM'а - непонятно)
ViacheslavNk
09.11.2024 17:59Если посмотреть на список параметров OpenSCManager то там есть lpMachineName, это возможность удаленной работы с сервисами. В одной из наших фичей в бизнес логике была функциональность загрузки бинарных файлов на шару, удаленная регистрация сервиса, запуск сервиса, какая то работа, остановка сервиса, удаление. И с тысячами различных конфигураций кастомеров, разных протоколов NTLM, Kerberos разных с файрволов и пр, иногда в случае работы удаленно по сети, практически любая функция из SCM могла зависнуть или отвалиться с ошибкой на ровном месте.
Для того что бы ваша программа не зависла вместе с SCM нужно как-то асинхронно делать вызов, самый простой вариант все это выносить в отдельн6ый поток, но тут тоже множество подводных камней с правильной обработкой и передачей ошибок/исключений из одного потока в другой, что делать с зависшим потоком, терминировать или складывать в “отстойник” и т.д.
В вашей статье нет совершенно никакого намека на С++, просто вызов WinApi функций на Си с глобальными переменными и пр.
Для работы на С++ нужно все-таки делать какие-то абстракции, интерфейсы, самым первым, собственно, напрашивается
class SCManager, следом класс Service , со стороны реализации непосредственно сервиса так же напрашивается универсальный шаблон типа class TService, где в качестве параметров шаблона задаётся основная бизнес функция сервиса.
Очень важно подсветить безопасность всей этой истории, у вас сервис по умолчанию будет запушен под Local System, что дает очень много прав и если у вас в этом сервисе крутить какой ни будь RPC сервер, то это очень “тонкое” место с возможностью повышения привилегий.
Можно так же рассмотреть использование Managed Service Account и Group Managed Service Accounts.
В целом ваша статья не плохая, но и не особа информативная, относительно MSDN.
kiff2007200
09.11.2024 17:59Намного легче написать службу на c# где поддержка этого намного удобнее и из него уже запускать c++ код
dan_sw Автор
09.11.2024 17:59Думаю да, это было бы легче, учитывая что слои абстракции в C# вырастают с каждой новой версией этого языка так, что можно разработать службу значительно быстрее.
Однако использовать два языка программирования (C++ и C#) - не всегда уместно, поскольку программы на разных языках программирования имеют свои особенности, которые могут зависеть от платформы (в следствие чего поддержка таких программ требует больших усилий), да и скрытие "рутинных задач" за большим слоем абстракции лишает программиста важного - получения опыта. Ведь разобраться как устроено управление службами на C++, на мой взгляд, ценнее, чем использовать высокоуровневые абстракции и просто решить задачу так, как проще или так, как "модно".
Конечно, и в статье используется высокоуровневые абстракции из WinAPI. Может быть есть способ написать службу на более низком уровне с использованием чистого Си и без WinAPI, однако такого способа я, на данный момент, не знаю. Если бы знал такой способ - сделал бы на чистом Си) Уменьшая слои абстракции и используя более низкоуровневые инструменты программист получает больше опыта за счёт необходимости разбираться в большем числе деталей.
emusic
09.11.2024 17:59высокоуровневые абстракции из WinAPI. Может быть есть способ написать службу на более низком уровне с использованием чистого Си и без WinAPI
Вы о чем вообще?
dan_sw Автор
09.11.2024 17:59Прямо о том, о чём я написал в комментарии.
WinAPI - это ведь API, который является по сути набором высокоуровневых абстракций для программирования под Windows. Возможно есть способ написать службу и без использования WinAPI на чистом Си. Для этого нужно разработать какие-то свои похожие на WinAPI функции для работы с SCM. Грубо говоря повторить реализацию WinAPI, которая нужна для написания служб) Я об этом.
emusic
09.11.2024 17:59А я о том, что Вы даже толком не понимаете, что такое WinAPI, и что там за "высокоуровневые абстракции". А выражение "без использования WinAPI на чистом Си" - это вообще шедевр. Скажите честно, какая часть написанного Вами сгенерирована ИИ?
dan_sw Автор
09.11.2024 17:59Вы даже толком не понимаете, что такое WinAPI
Не могли бы Вы рассказать что такое WinAPI? Интересно узнать, чем Ваше понимание отличается от этого или вот этого понимания WinAPI.
Прежде чем на кого-то "указывать пальцем" убедитесь в том, что первый на кого Вы укажите должны быть не Вы.
Вы то точно понимаете как работает WinAPI, разницу между Си и C++, и можете "закрытыми глазами" определить пишет другой человек на C++ или на Си, верно? А если понимаете - поделитесь своими мыслями. А то никакой конкретики, если честно, я не услышал. Сплошная критика ради критики. Никаких замечаний по коду.
Если считаете что служба написана некорректно и где-то её можно поправить - дайте замечания программному коду и укажите где конкретно можно его поправить и как. Это будет гораздо ценнее, чем критика ради критики.
Скажите честно, какая часть написанного Вами сгенерирована ИИ?
Никакая, сам всё писал и в том числе комментарии. Не верите? Это нормально, сейчас очень много статей которые пишутся с помощью ИИ. По мне так такой труд не приносит пользы ни читателю, ни самому автору.
emusic
09.11.2024 17:59Не могли бы Вы рассказать что такое WinAPI?
Зачем мне это рассказывать, когда это стандартный и ходовой термин, суть которого описана, в том числе, и по приведенным Вами ссылкам?
А вот фраза "написать службу на более низком уровне с использованием чистого Си и без WinAPI" не является ни стандартным, ни ходовым выражением, это Ваше собственное высказывание. Поэтому я и пытаюсь узнать, какой смысл Вы в него вложили, ибо для любого, понимающего, что такое WinAPI, и как он используется в C, эта фраза очевидно абсурдна, бессмысленна.
Никаких замечаний по коду
По коду я Вам уже высказал конкретные замечания. То, что он компилируется и как-то работает - не основание для того, чтобы публиковать его под видом руководства.
Если считаете что служба написана некорректно и где-то её можно поправить
Я считаю, что вся статья написана некорректно. На таком уровне уместно писать статьи о прикладном программировании, но никак не о системном. Когда неверный подход лежит в самой основе, править что-то в коде - как переставлять кровати в том борделе из анекдота.
dan_sw Автор
09.11.2024 17:59Что могу сказать (потому что спорить немного надоело) - это полностью Ваше мнение. Правильно оно или нет - покажет время. Может быть через года опыта я приду к тому, что этот туториал содержит в себе некорректные подходы и с радостью его поправлю. Но пока никакой конкретики от Вас я не увидел
mvv-rus
09.11.2024 17:59и что там за "высокоуровневые абстракции".
Таки позанудствую, не возражаете?
Ну, строго говоря, в данном конкретном случае API SCM - это таки более высокоуровневые абстракции для механизмов более низкого уровня: работы с реестром для установки/удаления/изменения конфигурации служб и обращения к точкам вызова процесса SCM (services.exe) по протоколу DCE RPC (AFAIK). То есть, теоретически, можно не пользоваться этим самым "высокоуровневым" API SCM, а использовать в программе чисто API реестра и API RPC вместе с описаниями интерфейсов точек вызова на IDL - их правда, ещё найти надо, или, скорее сделать (AFAIK они не дркументированы), и поменяться они, как все недокументированное могут от версии к версии Windows. Но принципиальная возможность работы с сервисами на более низком уровне таки есть.
Но что-то, правда, заставляет меня сомневаться, что автор статьи писал слово "высокоуровневый", понимая все это, а не просто с намерением простыми, универсальными, но малозначащими словами отделаться от критики (ну, или его ИИ набрался таких словечек во всяких интернетах).emusic
09.11.2024 17:59API SCM - это таки более высокоуровневые абстракции для механизмов более низкого уровня: работы с реестром для установки/удаления/изменения конфигурации служб
Строго говоря, это разные вещи. Реестр - это просто база данных, в которой хранятся сведения о службах, а SCM отвечает за управление этой базой. По-хорошему, непосредственная работа с базой в обход SCM вообще не должна была допускаться, но так уж вышло, что в MS сами использовали обходные пути, поэтому вынуждены и дальше тянуть совместимость.
А абстракции в API SCM ничуть не больше, чем в реестровых записях.
обращения к точкам вызова процесса SCM (services.exe) по протоколу DCE RPC (AFAIK)
Здесь абстракция действительно имеет место. Осталось понять, каким боком чистый C мог бы способствовать ее обходу - при том, что примеры фактически на нем и написаны. :)
Но что-то, правда, заставляет меня сомневаться, что автор статьи писал слово "высокоуровневый", понимая все это, а не просто с намерением простыми, универсальными, но малозначащими словами отделаться от критики (ну, или его ИИ набрался таких словечек во всяких интернетах).
О том и речь, что автор взял на себя задачу, адекватно вывезти которую он не в состоянии. Ему еще учиться и учиться до того, как учить кого-то другого.
dan_sw Автор
09.11.2024 17:59О том и речь, что автор взял на себя задачу, адекватно вывезти которую он не в состоянии. Ему еще учиться и учиться до того, как учить кого-то другого.
Ну, у меня отличные помощники в комментариях ;)
Читатель может узнать о том, куда глубже копнуть или что лучше изучить на основе критики, а это конечно отлично скажется на понимании материала или его дополнении, что безусловно только плюс)
Вы посмотрите сколько Вы уже всего написали в комментариях. Может быть это и критика ради критики, но, кого-то да направит на "путь истинный" :)
dan_sw Автор
09.11.2024 17:59Интересная информация)
Но что-то, правда, заставляет меня сомневаться, что автор статьи писал слово "высокоуровневый", понимая все это, а не просто с намерением простыми, универсальными, но малозначащими словами отделаться от критики (ну, или его ИИ набрался таких словечек во всяких интернетах).
Про протокол DCE RPC (AFAIK) я не слышал, но знаю, что RPC обозначает удалённый вызов процедур. Где-то об этом уже читал (конкретно об использовании RPC в SCM), но действительно не углублялся в такие особенности.
С WinAPI я знаком на поверхностном уровне достаточном для решения прикладной задачи, не более. Я не утверждал, что я эксперт в WinAPI, я лишь понимаю на высоком уровне что это, в широком смысле, набор функций для работы с элементами Windows.
И я не ИИ. (и тут у кого-нибудь закралась мысль, что я всё это пишу с ИИ, потому что только ИИ будет отрицать что он ИИ :))
kiff2007200
09.11.2024 17:59которые могут зависеть от платформы
В чем проблему написать простейшую службу, запускающую ехе, и скомпилить ее под x86?
dan_sw Автор
09.11.2024 17:59Не вижу проблем. А если служба не простая, а сложная? И ещё взаимодействует со множеством других служб?
mvv-rus
09.11.2024 17:59Работа проделана, не спорю, но часть из нее IMHO - лишняя. А поскольку статья явно рассчитана на начинающих, эту часть можно было бы и не публиковать и, тем самым, сократить статью - и это IMHO пошло бы ей на пользу: сейчас великовата она получилась.
В Windows для работы со службами уже есть встроенная в систему обвязка: комнда sc.exe либо группа команд *-Service в Powershell. Если их использовать, то не нужно писать всю обвязку для установки и удаления службы - все делается через sc либо *-Service, а для работы самой службы всё это излишне: достаточно было бы оставить только регистрацию процесса службы в SCM (StartServiceCtrlDispatcher) с блоком точек входа в сервисы (в данном случае сервис - один) в main ( а не в _tmain, которая, кстати, только запутывает тех, кто знает C/C++), эту самую функцию входа в сервис (которая вызывает RegisterServiceCtrlHandlerEx, регистрирующий функцию обратного вызова, через которую SCM передает команды) и упомянутую функцию обратного вызова.
И мучиться с Get-WMIObject (который, кстати, обычно сокращают до gwmi) для работы со службами тоже не нужно - упомянутые команды куда удобнее. Ну, а запуск и остановку службы, совмещенную с процессом ожидания завершения этой операциии вообще удобнее делать командами net start и net stop.PS Ну и, побрюзжу, мне это вполне по возрасту.
В результате поиска справочных материалов, примеров реализации служб и литературы для полноценного понимания их разработки я столкнулся с проблемой разрозненности информации в разных источниках. Узнать как полноценно разработать службу на Windows используя только один источник - сложная задача. Даже в официальной документации Майкрософт все примеры разрознены и разбросаны по разным страницам и чтобы снабдить своё приложение основными функциями приходится искать и соединять разные блоки кода из разных примеров.
Странно мне это. Когда давным-давно
в далекой-далекой галактикея делал свой первый сервис, мне почему-то вполне хватило доступной документации из MSDN Library: общего описания жизненного цикла сервисов и документации по конкретным API. Может быть, это потому, что примеров я и не ждал: сервис я писал на первой 32-разрядной Delphi (так было надо), под которую примеров не завезли да и не ожидалось их. И классов, работу с сервисом облегчающих, в той Delphi не было тоже. Так что, пришлось разбираться чисто по описанию, как оно должно работать, и делать все самому. Впрочем, тогда такое положение было нормальным и даже хорошим: куда хуже делать что-либо вообще без документации - а порой приходилось. Правда, с другой стороны, тогда из службы можно было невозбранно вывести MessageBox на рабочий стол, а сейчас с этим проблемы.Но тем, кто привык разрабатывать с помощью копипасты кусков примеров - им таки эта статья, наверное, будет полезной. Посему я воздержусь от ее оценки.
HemulGM
09.11.2024 17:59А сейчас в Delphi из коробки есть шаблон сервиса со всеми обвязками. Буквально сразу можно писать только код полезной нагрузки.
dan_sw Автор
09.11.2024 17:59Думаю, что использование таких уже готовых шаблонов может быть уместно тогда, когда программист имеет полное понимание как эти службы вообще устроены, как они работают и как ими управлять из своей программы на более низком уровне.
В статье я не ставил цель "ускорить процесс разработки служб на Windows" потому что понятно, что для этого можно использовать уже готовые средства и инструменты заложенные в Delphi или C#. Так будет проще, быстрее и не нужно думать о том, как там всё внутри устроено. Однако такой вариант меня не устраивает, немного надоело, что везде всё уже сделано, и что "нужно использовать готовые решения".
Готовые решения это конечно хорошо, но сделать что-то самому с нуля, разобраться во множестве тонкостей и решить кучу проблем - это очень полезно.
durnoy
09.11.2024 17:59Имхо, самый важный вопрос от комментатора выше (и вопрос, который возник у меня) это зачем нужно самому реализовывать install/start/stop/uninstall? Ведь есть уже встроенные утилиты, которые это делают. Для понимая устройства сервисов новичком этот код ничего не даёт, а только отвлекает от собственно тела сервиса.
Статья на этот вопрос не отвечает, запутывая читателей. Для новичков слишком много информации сразу. Для не новичков -- непонятно, зачем.
В крайнем случае, эти примеры можно было бы вынести в приложение в конце.
dan_sw Автор
09.11.2024 17:59Для понимая устройства сервисов новичком этот код ничего не даёт, а только отвлекает от собственно тела сервиса.
Я так не считаю, поэтому акцентировал особое внимание на реализации этих функций. Если можно эти функции контролировать, то почему бы этого не сделать? Может быть в этих функциях какие-то дополнительные действия необходимы (например, логирование). Да и после реализации этих функций будет понимание того, как уже готовые утилиты (примерно) запускают службы.
Статья на этот вопрос не отвечает, запутывая читателей.
Не понимаю где тут может быть путаница. Сначала я описал что такое службы и как они работают, а затем описал реализацию приложения, которая управляет службой и, собственно, саму точку входа в службу. Всё, вроде бы, последовательно.
Для не новичков -- непонятно, зачем
Статья не для совсем новичков, а уже тех, кто более менее понимает как программировать приложения и столкнулся с задачей написания службы для Windows.
ZetaTetra
09.11.2024 17:59Хоть службы и лишены пользовательского интерфейса, но они могут пригодится при работе с этим самым пользовательским интерфейсом
А как же Allow windows service to interact with desktop?
https://learn.microsoft.com/en-us/windows/win32/services/interactive-services
Да и MessageBox можно бахнуть в SYSTEM desktop ради лулзов в любой момент
emusic
09.11.2024 17:59Лет тридцать назад подобная статья могла бы представлять какую-то ценность, но сейчас - увы.
В заголовке претенциозно заявлено"на C++", а за основу взяты древние тексты на чистом C, к которым сбоку прикручены отдельные элементы C++ вроде вызовов logger. Везде трудолюбиво используются типы указателей с префиксом LP которые потеряли смысл сразу после перехода с 16-разрядной архитектуры на 32-разрядные.
Совершенно непонятно, для чего нужно столь подробное разжевывание. Системные службы следует делать программистам, имеющим достаточную квалификацию, а для таких более чем достаточно уже имеющейся документации.
Статья может быть полезна разве что прикладникам на каких-нибудь JavaScript/Python, которые про C++ только слышали краем уха, но которым приспичило наваять службу, дабы значок их приложения всегда был в области уведомлений. Вероятность использования этого материала для создания сколько-нибудь адекватной и полезной службы, не создающей проблем с надежностью и безопасностью, стремится к нулю. :(
dan_sw Автор
09.11.2024 17:59Лет тридцать назад подобная статья могла бы представлять какую-то ценность, но сейчас - увы.
Почему? И сейчас встречаются реальные задачи где нужно разработать службу для Windows или Linux. Я, например, сейчас такой задачей и занимаюсь. Если бы я где-то аналогичный материал нашёл, то для меня этот материал был бы ценен.
Ценность материала - штука очень субъективная. Для опытных профессионалов эта статья может нести нулевую ценность из-за того, что они всё это уже знают, а для новичков в этой теме она ценна.
Тип данной статьи - туториал, я не зря же его таким поставил. Туториал предполагает, что будет решена какая-то полезная задача пошагово. Ровно это я и делаю - решаю интересную мне задачу пошагово, с описанием всех своих действий чтобы интересующийся читатель мог эти действия воспроизвести и получить полезный результат в виде работающего приложения для управления службой на Windows.
В заголовке претенциозно заявлено "на C++", а за основу взяты древние тексты на чистом C, к которым сбоку прикручены отдельные элементы C++ вроде вызовов logger.
Вообще, я использую в данном проекте C++. Это можно легко понять и по исходным файлам где определяются функции для службы - они используют расширение *.cpp, а не *.hpp, которые обычно используют для исходников на чистом Си.
Общеизвестно что C++ старается сохранить обратную совместимость с Си, на базе которого этот язык и построен. Однако если я использую Си код в проекте на C++ это не значит, что я везде использую чистый Си. Я мог бы добавить классы в файле WinService.h и все функции сделать методами и тогда не получать замечания по поводу того, что я C++ использую, а не Си) Но я это сделал намерено для упрощения, потому что на C++ можно писать код в любом стиле, в том числе Си-стиле, что я и сделал.
И для объективности, если Вы сейчас зайдёте в репозиторий dev-win-sc, то увидите, что там C++ кода больше, чем кода на Си:
Как видите даже CMake инструкций в проекте больше, чем чистого кода на Си) А я сомневаюсь, что GitHub сильно ошибается в оценке кодовой базы, а если и ошибается - нужны пруфы, которых у меня нет (если у Вас есть - было бы интересно с ними ознакомится).
Везде трудолюбиво используются типы указателей с префиксом LP которые потеряли смысл сразу после перехода с 16-разрядной архитектуры на 32-разрядные.
Я использовал части кода из официальной документации Microsoft, которая обновлялась сравнительно недавно (13.06.2023, если верить содержимому их статей), поэтому сомневаюсь что префикс LP потерял свою актуальность. Если в официальной документации используются устаревшие и нерабочие элементы API, то согласитесь, такая документация была бы вредна и люди вообще бы не смогли сами такие службы разрабатывать, просто потому что актуальной инфы по этому поводу нет? Нельзя же наугад что-то делать, нужны хоть какие-то зацепки. Поэтому считаю что данное замечание не корректно.
Совершенно непонятно, для чего нужно столь подробное разжевывание
Честно говоря, я бы разжевал ещё более подробнее и глубже, но статья и так получилась большой, поэтому разжевал так, чтобы я или читатель вернувшись к этой статье через 1-2 или более лет смог совершенно спокойно понять о чём тут идёт речь и как разработать службу на C++.
Системные службы следует делать программистам, имеющим достаточную квалификацию, а для таких более чем достаточно уже имеющейся документации
Каких "таких"? Не понятно под кем Вы понимаете "таких". Может быть "такие" программисты тоже хотят стать квалифицированными специалистами в этой области и чтобы освоить эту тему такой материал им может быть полезен. Тем более я постарался дать теоретическую базу по службам в Windows, прежде чем приступить к их разработке чтобы было более понятно что они вообще есть такое и зачем нужны.
Статья может быть полезна разве что прикладникам на каких-нибудь JavaScript/Python, которые про C++ только слышали краем уха, но которым приспичило наваять службу, дабы значок их приложения всегда был в области уведомлений
Ну вот, уже хоть для кого-то данная статья имеет ценность) Служба - это не про "значок в области уведомлений", это про осуществление полезной работы в фоновом режиме. Да и программисты на JavaScript/Python/<любой другой язык кроме C/C++> может быть заинтересован в получении опыта работы на C++ для совершенно разных целей. Может быть интерес к самому языку, к работе на данном языке или повышение квалификации. Не думаю, что программист с минимальными знаниями C++ сможет хорошо понять то, что в статье происходит. Всё таки для её чтения нужно иметь хотя бы "средние" знания C++ и как на нём разрабатывать полноценные приложения.
Вероятность использования этого материала для создания сколько-нибудь адекватной и полезной службы, не создающей проблем с надежностью и безопасностью, стремится к нулю. :(
Не могли бы Вы рассказать о проблемах, которые связаны с надёжностью и безопасностью в изложенном мной материале? Было бы неплохо его улучшить, если Вы действительно знаете где в исходном коде есть потенциальные проблемы и как от них избавится, я бы мог внести правки в свой материал и те, кто будет его читать получат возможность побольше узнать об устройстве служб и как делать "не нужно".
На данный момент я все дескрипторы освобождаю и делаю все возможные проверки на разных этапах работы управляющих функций и на данный момент не вижу проблем с надёжностью и безопасностью, может быть Вы на них прольёте свет я буду этому только рад.
emusic
09.11.2024 17:59Для опытных профессионалов эта статья может нести нулевую ценность из-за того, что они всё это уже знают, а для новичков в этой теме она ценна
Службы, драйверы ядра и подобное - темы не для новичков, это аксиома. Странно, что Вы этого не понимаете. Написав эту статью, Вы повысили вероятность того, что однажды в Вашей системе появится очередная кривая служба, слепленная на коленке одним из таких новичков, который без Ваших подробных разъяснений этого не осилил бы.
Тип данной статьи - туториал, я не зря же его таким поставил. Туториал предполагает, что будет решена какая-то полезная задача пошагово
Не все туториалы одинаково полезны. Например - "пошаговое руководство по сборке авиационной бомбы на кухне".
Вообще, я использую в данном проекте C++. Это можно легко понять и по исходным файлам где определяются функции для службы - они используют расширение *.cpp
Да, это примерно как делать и запускать под Windows исключительно DOS-программы, но при этом утверждать "я работаю и программирую под Windows". :)
Я мог бы добавить классы в файле WinService.h и все функции сделать методами и тогда не получать замечания по поводу того, что я C++ использую, а не Си)
Не нужно искусственно добавлять классы там, где они не нужны. Замечания в основном в отношении того, что в своем "коде на C++" Вы используете то, ради ухода от чего и создавался C++ - объявление переменных задолго до инициализации, заворачивание понятного const_cast в невнятный макрос, NULL вместо nullptr, никому не нужный void в качестве формального параметра и прочее. Уж на сколько я сам ретроград в отношении C++, но Ваши тексты откровенно режут глаза.
Я использовал части кода из официальной документации Microsoft, которая обновлялась сравнительно недавно
То, что они регулярно обновляют даты, не означает, что обновляется содержимое. Там многое не обновлялось уже лет тридцать.
сомневаюсь что префикс LP потерял свою актуальность
Актуальность он потерял еще тогда, когда от short/long pointer перешли к flat pointer (то есть, от Win16 к Win32). Хоть под Win16 служб никогда и не было, но MS в своих заголовках для NT традиционно дублировали типы указателей P* типами LP*. Они настолько же "актуальны", как и (void) в определении/объявлении функции - синтаксически корректны, но ни малейшего смысла не имеют.
Может быть "такие" программисты тоже хотят стать квалифицированными специалистами в этой области и чтобы освоить эту тему такой материал им может быть полезен
Чтобы стать достаточно квалифицированным для разработки системных программ, программисту необходимо читать серьезную профильную литературу, а не "пошаговые туториалы". В следующий раз, когда Вам встретится очередной кривой драйвер, инсталлятор, "оптимизатор реестра" или что-нибудь подобное - постарайтесь не забыть, что Вы лично внесли свою лепту в эту тенденцию.
dan_sw Автор
09.11.2024 17:59Написав эту статью, Вы повысили вероятность того, что однажды в Вашей системе появится очередная кривая служба, слепленная на коленке одним из таких новичков, который без Ваших подробных разъяснений этого не осилил бы
Вообще не понимаю причём тут данный туториал и "очередная кривая служба". Если она действительно кривая, то будьте добры объяснить в чём эта кривизна проявляется. Мне на данный момент это не понятно, как и какие могут здесь возникнуть проблемы с безопасностью.
Не все туториалы одинаково полезны. Например - "пошаговое руководство по сборке авиационной бомбы на кухне".
Согласен. Можно сделать его максимально полезным, если Вы поделитесь своим профессиональным опытом написания служб на C++ и расскажите какие минусы в представленной в статье реализации есть ("кривизна", "плохая безопасность").
В следующий раз, когда Вам встретится очередной кривой драйвер, инсталлятор, "оптимизатор реестра" или что-нибудь подобное - постарайтесь не забыть, что Вы лично внесли свою лепту в эту тенденцию.
Отнюдь, не вижу связи. В туториале рассматривается самое базовое приложение для управления службами ну и сама служба. Если программист пишет "кривой драйвер", то наверное ему его нужно исправить? А как написать "не кривой драйвер" если ты никогда не ошибался при его написании? Иными словами чтобы написать хороший драйвер нужно ошибаться, но необязательно. Ошибается ли среднестатистический программист при написании драйвера? Да, ошибается. Обязательно ли это? Нет, не обязательно. Вношу ли я прямо или косвенно вклад в то, что кто-то пишет кривой драйвер? Навряд ли, потому что в результате туториала создаётся базовая служба и приложения для управления ею, а не драйвер.
Чтобы программист стал профессионалом - ошибаться нормально, особенно когда программист готов эти ошибки исправлять и понимать почему это ошибка, собственно, возникает.
emusic
09.11.2024 17:59не понимаю причём тут данный туториал и "очередная кривая служба"
Связь самая непосредственная. Любые (без исключения) туториалы предназначены для тех, кто или вообще не понимает сути описываемой деятельности, или понимает ее чисто интуитивно, и сам не в состоянии понимать адекватные технические описания. Туториалы типа "как отключить обновления через редактор реестра" исключительно уместны, так как предназначены для обычных пользователей, для которых не предусмотрено более адекватных средств управления, и которым нужно отключить обновления в своей системе, а не написать программу, отключающую их в произвольной.
Ваш туториал по созданию службы практически бесполезен для профессионального системного программиста - вместо него он будет использовать техническую документацию. Но им охотно воспользуется любой, кто с помощью подобных же туториалов научился составлять относительно работоспособные программы, не особо понимая, что и как они делают, а лишь записывая в нужном порядке символы и слова. Когда такие ребята сваяют очередное кривое и глючное поделие, чтоб занять свою нишу на рынке - и черт бы с ними. Но с помощью таких, как Вы, они успешно лезут туда, где им не место.
как и какие могут здесь возникнуть проблемы с безопасностью
Элементарно: служба, которую по Вашему туториалу напишет дилетант, скорее всего, будет работать с файлами и/или реестром. Если он, замыслив удалить со всем содержимым какой-нибудь временный каталог или созданную им ветку реестра, перепутает пути, и сделает это с системными путями, то в обычном непривилегированном приложении он получит отлуп, а в службе этот код исправно отработает.
А если он сколхозит свою службу так же, как нынче принято писать пользовательские приложения, то в ней с высокой вероятностью будет изрядный набор удобных целей для атак.
если Вы поделитесь своим профессиональным опытом написания служб на C++
Каким конкретно опытом я мог бы поделиться в данном контексте? Я в этой сфере не придумал ничего нового - все давным давно описано вдоль и поперек.
Кстати, Вы хоть в курсе, что у типичного "современного программиста на C++" тексты Ваших примеров вызовут когнитивный диссонанс? :) Его очень удивит использование legacy-функций вместо string, отсутствие других привычных обращений к STL, первое присваивание переменным, определенным где-то выше, и т.п. Эти тексты адекватно поймет только тот, кто одновременно знает и C, и C++, причем C++ он изучал по материалам минимум двадцатилетней давности.
Если программист пишет "кривой драйвер", то наверное ему его нужно исправить?
Для начала неплохо бы ответить на вопросы "почему драйвер получился кривым?" и "действительно ли человек, написавший драйвер, является программистом?".
как написать "не кривой драйвер" если ты никогда не ошибался при его написании?
Уж точно - не писать драйвер по "туториалу". Для начала следует изучить все, что необходимо знать в этой сфере, и понимать, как устроен драйвер, в какой среде он работает, как с ним взаимодействует система, каковы типичные риски, и т.п. Все это неплохо описано и в документации MS, и в профильной литературе. Но мне попадались и "туториалы" вроде Вашего, в стиле "ваша программа не имеет доступа к портам? напишите драйвер, который это делает!". Соответственно, попадались и поделия, реализующие через глубокую кривую задницу то, что положено делать совсем иначе.
Ошибается ли среднестатистический программист при написании драйвера?
Среднестатистическому программисту вообще нечего делать в области драйверов и служб. А квалифицированные программисты, работающие в этих областях, и ошибаются соответственно - например, в том, что забыли инициализировать или освободить память, а не в том, что используют в программе конструкции, смысла которых не понимают.
dan_sw Автор
09.11.2024 17:59Элементарно: служба, которую по Вашему туториалу напишет дилетант, скорее всего, будет работать с файлами и/или реестром. Если он, замыслив удалить со всем содержимым какой-нибудь временный каталог или созданную им ветку реестра, перепутает пути, и сделает это с системными путями, то в обычном непривилегированном приложении он получит отлуп, а в службе этот код исправно отработает.
Я ну искренне не понимаю, причём тут данный туториал? Вы утверждали, что в коде есть проблемы с надёжностью и безопасностью в своём комментарии выше:
Вероятность использования этого материала для создания сколько-нибудь адекватной и полезной службы, не создающей проблем с надежностью и безопасностью, стремится к нулю.
Я же правильно всё понял? Или Вы что-то другое имели ввиду?
Я попросил Вас - укажите на мои ошибки. Неоднократно это делал, однако так и не дождался конкретики, сплошная теория. И сейчас Вы пишите мол "ошибка будет тогда, когда какой-нибудь программист напишет то и то, вот тогда будет беда", а про конкретно текущий код у Вас нет претензий, я правильно понимаю? Я что-то уже запутался если честно) Какая-то критика ради критики)
Если у Вас есть конкретные предложения по улучшению статьи - предлагайте, с радостью это сделаю. Если их у Вас нет (с каждым комментарием я в этом только убеждаюсь), то ок - зачем тогда критика? Ради критики? Она ведь далеко не всегда у Вас объективна, скорее очень очень субъективна и со многим из этой критики я не согласен, кроме указаний моих фактических ошибок (по типу ошибки понимания формата *.hpp как *.c).
А если он сколхозит свою службу так же, как нынче принято писать пользовательские приложения, то в ней с высокой вероятностью будет изрядный набор удобных целей для атак.
Видимо упоминания того, что службы работают в фоновом режиме было не достаточно. Ладно. Какой изрядный набор удобных целей для атак есть в описанном Вами случае? Интересно узнать.
Среднестатистическому программисту вообще нечего делать в области драйверов и служб
Почему? Есть какие-то ограничения? Программист (любой) может заинтересоваться этой областью и начать делать службы и писать драйвера. Возможно успешно, возможно не очень. Делать ему так определённо есть что, если ему это интересно.
Вы пытаетесь сформировать образ системного программирования как то, что доступно лишь "элите", "первоклассным стрелкам по ногам с помощью кольта C/C++", которые читают только жёсткую техническую литературу и знают точно-точно всё из своей области, потому что войти в неё сложно. Ну или чтобы в неё войти, надо прочитать кучу технической литературы.
Я ж не спорю. Системное программирование - действительно классная область, со своими проблемами, задачами и интересными особенностями. И да, чтобы в этой области хорошо разбираться нужно кучу технической литературы изучить и программировать низкоуровневые штуки. Но войти в эту область среднестатистический программист может, если захочет (и ничего Вы с этим не сделаете).
Боитесь конкуренции в системном программировании? Это лишнее, задач на всех хватит)
emusic
09.11.2024 17:59Даже если Вы не ИИ, то Вам отлично удается его имитировать.
dan_sw Автор
09.11.2024 17:59Даже если бы я был ИИ, то зачем мне это? Зачем мне писать эту статью с помощью ИИ или сгенерировать её полностью самим ИИ?
У меня ведь даже мотивации толком нет, чтобы генерировать статьи, польза от которых и для меня (человека), и для читателей (вот тут уже сложно - может быть и человек, а может быть и ИИ) может быть минимальна. Я наоборот стремлюсь к качеству материала и углублению как своих знаний, так и знаний читателя.
emusic
09.11.2024 17:59Я наоборот стремлюсь к качеству материала и углублению как своих знаний, так и знаний читателя
Даже если Вы искренне в это верите, то это, к сожалению, лишь иллюзия. То, что Вы способны писать пространные тексты и собирать в кучу обрывки разнородных материалов, не имеет отношения ни к качеству, ни к углублению знаний.
Если не хотите, чтобы над Вами смеялись - или углубляйте знания перед тем, как излагать их на публике, или излагайте только то, в чем уже достаточно глубоко разобрались и имеете опыт. Возможно, статьи по программированию на JS/TS у Вас получились бы значительно лучше - по крайней мере, сейчас.
dan_sw Автор
09.11.2024 17:59Даже если Вы искренне в это верите, то это, к сожалению, лишь иллюзия. То, что Вы способны писать пространные тексты и собирать в кучу обрывки разнородных материалов, не имеет отношения ни к качеству, ни к углублению знаний.
Вообще, любая вера, можно сказать, иллюзия. Но это уже плоскость философии, так что согласен. Кто на что горазд, так скажем)
Я собираю не обрывки, а описываю полноценный процесс создания служб, не более. А вот уж каким образом я углубляю знания - Вам навряд ли известно. Лично я разобрался в теме настолько, насколько требовалось и попытался до читателя донести усвоенный самим собой материал. Успешна эта попытка или нет - покажет время.
Если не хотите, чтобы над Вами смеялись - или углубляйте знания перед тем, как излагать их на публике, или излагайте только то, в чем уже достаточно глубоко разобрались и имеете опыт.
Спасибо за совет, но я сам лучше для себя решу что мне делать - публиковать материал или нет. Я не чувствую, что надо мной тут "смеются", скорее у читателей могут быть идеи по улучшению статьи, чему я всегда рад.
А если даже кто-то и смеётся, то это моя проблема или проблема тех, кто смеётся? :)))
Я свой уровень и свои возможности прекрасно знаю, есть ещё куда расти)
dan_sw Автор
09.11.2024 17:59Это можно легко понять и по исходным файлам где определяются функции для службы - они используют расширение *.cpp, а не *.hpp, которые обычно используют для исходников на чистом Си.
Здесь не корректно указал расширение - *.c, а не *.hpp (корректировка комментария не возможна из-за ограничений по времени)
HemulGM
09.11.2024 17:59Замечу все же, что GitHub крайне хреново определяет язык в файлах репозитория. Например, файлы inc, он безоговорочно считает как код на C++, однако, они много где используется. Например, в Delphi принято такое расширение для файлов, которые подключаются директивой
{$include 'file.inc'}
Есть даже целые репозитории, которые GitHub определяет как проект на C++, в то время, как он полностью на Pascal (Delphi).
emusic
09.11.2024 17:59Странно, ни разу не видел файлов .inc с кодом на C++. Это расширение всегда типично использовалось для вставок на ассемблере, в makefile, в том же Pascal и т.п.
HemulGM
09.11.2024 17:59https://github.com/ev1313/Pascal-SDL-2-Headers
Тут только файлы pas и inc в которых исключительно код на Delphi/Pascal
Результат:
А где там php вообще не понятно
orcy
09.11.2024 17:59Да не, довольно неплохо если такая статья будет находится поиском, например. Можно и по MSDN все сделать, но тот понемногу мутирует, непонятно что с ним в итоге будет. Более того MSDN это reference, а это как пример службы с хорошим описанием типовых вещей организации сервиса.
Совершенно непонятно, для чего нужно столь подробное разжевывание.
Вот такие комментарии вроде "автор ты больше такое не пиши" по моему абсолютно не корректные. Обычно автор сам в силах разобраться что ему писать или не писать. WinAPI программирование хоть и ушло из мейнстрима все еще актуально, есть кому опыт других интересен.
emusic
09.11.2024 17:59Вот из-за таких авторов Хабр и скатился от некогда вполне серьезного и уважаемого ресурса к унылому болоту, в котором что-то достойное бывает не каждую неделю. :)
dan_sw Автор
09.11.2024 17:59в котором что-то достойное бывает не каждую неделю
Что ж, если Вы так считаете, то почему бы Вам лично статью на данной платформе не написать? Почему нет? Расскажите о решении какой-нибудь задачи из области системного или прикладного программирования, напишите хорошую статью так, как Вы её видите. Покажите своим примером как нужно статьи писать)
Ведь рассуждать так любой горазд мол "унылое болото", "платформа скатилась" и т.д. и т.п., но почему-то показать своим примером "как надо делать" не у каждого получается. Или Вы только потребитель? Посмотрел на Ваши статьи в профиле и там нет ни одной публикации после 2015 года, зато комментируете другие статьи до сих пор. А оценки то у статей неплохие, почему бы Вам не продолжить публиковать материалы? Чем больше технического контента на платформе, тем лучше.
Ладно бы если эти комментарии были не критикой ради критики, а несли в себе что-то действительно важное, что можно подчерпнуть и добавить в само содержимое статьи (от этого, повторюсь, выигрывают все), но конкретно Ваша критика мне видится очень субъективной и по сути просто критика ради критики, типа хобби, а вот конкретно к коду никаких замечаний у Вас нет (кроме холиварной темы "что есть Си, а что есть C++", которую продолжать я не хочу, поскольку у каждого своё мнение на этот счёт и я останусь при своём).
Станьте тем автором Хабра, который будет писать "достойные статьи", покажите как надо) Желательно каждую неделю) Буду лично читать Ваши статьи и набираться опыта того, как надо писать такие статьи на темы системного программирования) (не сарказм, уверен у Вас есть чему поучиться).
danilasar
09.11.2024 17:59они используют расширение *.cpp, а не *.hpp, которые обычно используют для исходников на чистом Си.
Если автор отличает Си от C++ по расширению, да ещё утверждает, что hpp якобы используется для исходников на чистом Си, у меня возникают очень серьёзные вопросы к его компетенции в этом вопросе. Настолько серьёзные, что на низкопробный легаси код, приведённый в статье, уже даже не смотришь.
Работа и вправду проделана большая, но выставленный автором высокий уровень сложности совершенно не соответствует действительности. Это, несмотря на размер, достаточно простая статья, причём весьма низкого качества. Я бы категорически не рекомендовал её использовать будущим читателям как учебное пособие или что-то в этом роде.
Тем не менее, я хочу пожелать автору успехов, при должном упорстве он, конечно, многого может добиться, особенно если прислушается к критике со стороны более опытных людей, уже высказавших свои основные замечания в комментариях.
Я лишь отмечу, что любой Си-код является валидным C++-кодом, а суть C++ состоит не только в другом расширении и не только в использовании операторов ввода-вывода) Рекомендую почитать на досуге исходные коды известных Си и C++ библиотек, да и в принципе более подробно ознакомиться с основами обоих языков, сходствами и различиями между ними. Расширение у Вас, может быть, и .cpp, но C++ разработчики так не пишут и Вам на это напрямую указал @emusic
emusic
09.11.2024 17:59C++ разработчики так не пишут
А с этим другая засада. :) C++ разработчики, выросшие на современной литературе, чаще всего не мыслят C++ без STL, многоуровневых шаблонов, исключений, лямбд и прочего, даже если что-то из этого явно избыточно. Еще неизвестно, сможет ли типичный C++ - прикладник, осваивающий системное программирование, написать код лучше того, что мы видим здесь. :)
dan_sw Автор
09.11.2024 17:59Если автор отличает Си от C++ по расширению, да ещё утверждает, что hpp якобы используется для исходников на чистом Си
Да ошибся я, ну не могу поправить комментарий) Расширение hpp не используется для исходников на чистом Си)
Работа и вправду проделана большая, но выставленный автором высокий уровень сложности совершенно не соответствует действительности. Это, несмотря на размер, достаточно простая статья, причём весьма низкого качества. Я бы категорически не рекомендовал её использовать будущим читателям как учебное пособие или что-то в этом роде.
Можете пожалуйста подробнее раскрыть суть предложения "причём весьма низкого качества"? Я бы хотел эту статью улучшить, если она действительно получилась не очень хорошего качества. Почему она низкого качества и чего в ней не хватает? Что бы Вы хотели видеть в данной статье, чего ещё нет? Любые предложения по её улучшению принимаются)
Простая статья может быть для конкретно Вас, но тех, кто в первый раз работает с такой задачей - это может быть сложно. Я не из-за размера её уровень поставил, а из-за содержимого, потому как посчитал его сложным. Но, это уже моё субъективное мнение) Оценка сложности - дело индивидуальное
Einherjar
Все что вы пишете в этом абзаце как то ориентировано на 32-битные процессы, которые уже мало кому нужны, виндовс 11 так вообще 32 битной нет даже. В x64 все работает иначе, и регистры по другому называются даже. Да и смысла указывать calling convention особого нет, он там один фактически кроме разве что всяких редких случаев с vectorcall.
orcy
Если нужен один бинарник службы то думаю по прежнему проще собрать 32. Будет работать как в 32-битных так и 64-битных версиях windows.