SCM - это сервер, реализованный в Windows, для удаленного управления сервисами (вызовом процедур).
Для того, чтобы запустить драйвер в Windows, ему в соответствие ставится сервис, который обеспечивает управление этим драйвером. Не путать с устройством, которое создает драйвер в системе, через которое происходит обмен сообщениями с драйвером. Это устройство создается уже после старта драйвера, а вот SCM обеспечивает само внесение драйвера в систему. С помощью SCM можно: добавлять, удалять, запускать или останавливать службы.
Постановка задачи
Написать буферный класс позволяющий упростить работу SCM в C#.
Сам внешний вид этого класса можно обознать очень просто:
public ref class ServiceControlManager : public IDisposable
{
public:
ServiceControlManager(void);
void AddDriver(String^ ServiceName, String^ BinaryPathName);
void DeleteDriver(String^ ServiceName);
void StartDriver(String^ ServiceName);
void StopDriver(String^ ServiceName);
protected:
~ServiceControlManager();
!ServiceControlManager();
private:
SC_HANDLE SCMHandle;
};
Конструктор, деструктор, финализатор, основные методы, из атрибутов только HANDLE объекта SCM. Из этого следует, что экземпляр объекта этого класса будет содержать в себе созданный объект SCM, а методы упрощают с ним работу. Класс является буферным, и поскольку он реализован в C++/cli он будет автоматически масштабируем для работы в среде .NET, соответственно и в C#.
Решение проблемы с ошибками
Основная проблема работы с таким классом - это возвращение кодов ошибок, которые произошли в ходе работы SCM, которое желательно на самом первом этапе работы заменить на более привычные для .NET среды исключения. Для этого можно создать подобный класс:
[Serializable]
public ref class KernelErrorException : Exception
{
public:
KernelErrorException(void);
virtual String^ ToString() override;
property virtual String^ Message
{
String^ get() override;
};
property virtual DWORD Errorsource
{
DWORD get();
};
private:
DWORD errorsource;
internal:
KernelErrorException(DWORD Errorsource);
};
Как мы видим, экземпляр этого класса будет содержать, как атрибут только номер кода, который будет получен от GetLastError(). А при попытке привести экземлляр к типу System::String выведет полный текст описания сообщения средствами Windows.
Класс имеет два конструктора, первый - по умолчанию: сохраняет код ошибки при выполнении. Второй - получает код ошибки, как аргумент. Второй необходимо использовать в тех случаях, когда необходимо вызвать исключение, но перед этим выполнить какие-либо действия, после которых команда GetLastError() вернет не верные значения. Для этого сохраняется код ошибки, выполняются действия, затем вызывается исключение. Пример таких действий можно найти ниже: очиста PTR, исползуемой для маршалинга (PTR необходимо очистить до вызова исключения, т.к. вернуться к этому куску кода в дальнейшем не получится).
KernelErrorException::KernelErrorException(void)
{
this->errorsource = GetLastError();
}
KernelErrorException::KernelErrorException(DWORD Errorsource)
{
this->errorsource = Errorsource;
}
При этом реализация методов будет самой, что ни на есть элементарной:
String^ KernelErrorException::Message::get()
{
LPTSTR message = NULL;
FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER / FORMAT_MESSAGE_FROM_SYSTEM,
NULL,
this->errorsource,
MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
(LPTSTR)&message,
0,
NULL);
String^ messageString = gcnew String(message);
LocalFree(message);
return messageString;
}
DWORD KernelErrorException::Errorsource::get()
{
return this->errorsource;
}
String^ KernelErrorException::ToString()
{
return this->Message::get();
}
SCM не бессмертный
UPG: Вторая проблема работы с SCM в .NET: handle SCM не может жить долго, иначе это приведет к зависанию системы. Поэтому при использовании необходимо следить за тем, чтобы удалением занимался не сбощик мусора, а сам программист. Придется строго описать конструктор и финализатор, в деструкторе же, по логике Dispose-паттерна, вызывается финализатор [спасибо GraD_Kh]. В финализаторе описывается высвобождение unmanage объектов:
ServiceControlManager::ServiceControlManager(void)
{
this->SCMHandle = OpenSCManager(NULL, NULL, SC_MANAGER_ALL_ACCESS);
if (!this->SCMHandle)
throw gcnew KernelErrorException();
}
ServiceControlManager::~ServiceControlManager()
{
this->!ServiceControlManager();
GC::SuppressFinalize(this);
}
ServiceControlManager::!ServiceControlManager()
{
if (!CloseServiceHandle(this->SCMHandle))
throw gcnew KernelErrorException();
}
Реализация
Реализация всех методов очень проста, основа ее - это вызов конкретной соответствующей процедуры, но корректное выполнение обязательно нуждается во всех проверках на исключительные ситуации. Поскольку этот класс является буферным, выносить в аргументы можно не только те ключевые поля, которые указал я, но я подобрал параметры для запуска самых стандартных драйверов…
void ServiceControlManager::AddDriver(String^ ServiceName, String^ BinaryPathName)
{
IntPtr serviceNamePtr = Marshal::StringToHGlobalUni(ServiceName);
IntPtr binaryPathNamePtr = Marshal::StringToHGlobalUni(BinaryPathName);
SC_HANDLE SCMHandleService = CreateService(this->SCMHandle,
(LPCTSTR)serviceNamePtr.ToPointer(),
(LPCTSTR)serviceNamePtr.ToPointer(),
SERVICE_ALL_ACCESS,
SERVICE_KERNEL_DRIVER,
SERVICE_DEMAND_START,
SERVICE_ERROR_NORMAL,
(LPCTSTR)binaryPathNamePtr.ToPointer(),
NULL, NULL, NULL, NULL, NULL);
DWORD errorsource = GetLastError();
Marshal::FreeHGlobal(serviceNamePtr);
Marshal::FreeHGlobal(binaryPathNamePtr);
if (!SCMHandleService)
throw gcnew KernelErrorException(errorsource);
if (!CloseServiceHandle(SCMHandleService))
throw gcnew KernelErrorException();
}
void ServiceControlManager::DeleteDriver(String^ ServiceName)
{
IntPtr serviceNamePtr = Marshal::StringToHGlobalUni(ServiceName);
SC_HANDLE SCMHandleService = OpenService(this->SCMHandle,
(LPCTSTR)serviceNamePtr.ToPointer(),
SERVICE_ALL_ACCESS);
DWORD errorsource = GetLastError();
Marshal::FreeHGlobal(serviceNamePtr);
if (!SCMHandleService )
throw gcnew KernelErrorException(errorsource);
if (!DeleteService(SCMHandleService))
throw gcnew KernelErrorException();
if (!CloseServiceHandle(SCMHandleService))
throw gcnew KernelErrorException();
}
void ServiceControlManager::StartDriver(String^ ServiceName)
{
IntPtr serviceNamePtr = Marshal::StringToHGlobalUni(ServiceName);
SC_HANDLE SCMHandleService = OpenService(this->SCMHandle,
(LPCTSTR)serviceNamePtr.ToPointer(),
SERVICE_ALL_ACCESS);
DWORD errorsource = GetLastError();
Marshal::FreeHGlobal(serviceNamePtr);
if (!SCMHandleService)
throw gcnew KernelErrorException(errorsource);
if (!StartService(SCMHandleService, 0, 0))
throw gcnew KernelErrorException();
if (!CloseServiceHandle(SCMHandleService))
throw gcnew KernelErrorException();
}
void ServiceControlManager::StopDriver(String^ ServiceName)
{
IntPtr serviceNamePtr = Marshal::StringToHGlobalUni(ServiceName);
SC_HANDLE SCMHandleService = OpenService(this->SCMHandle,
(LPCTSTR)serviceNamePtr.ToPointer(),
SERVICE_ALL_ACCESS);
DWORD errorsource = GetLastError();
Marshal::FreeHGlobal(serviceNamePtr);
if (!SCMHandleService)
throw gcnew KernelErrorException(errorsource);
SERVICE_STATUS serviceStatus;
if (!ControlService(SCMHandleService, SERVICE_CONTROL_STOP, &serviceStatus))
throw gcnew KernelErrorException();
if (!CloseServiceHandle(SCMHandleService))
throw gcnew KernelErrorException();
}
Первый метод связывает sys файл с сервисом, добавляя этот сервис в систему. Второй - удаляет драйвер из системы, остальные две - запускают и останавливают сервис, соответственно.
Примеры использования в C#:
try
{
using (ServiceControlManager scm = new ServiceControlManager())
{
scm.AddDriver(serviceName, filePath);
scm.StartDriver(serviceName);
scm.StopDriver(serviceName);
scm.DeleteDriver(serviceName);
}
}
catch (Exception ex)
{
}
Настройки при компиляции
Самое главное не забывать постояно использовать маршалинг между управляемой и не управляемой кучей. Напомню, для маршаллинга необходимо находится в пространстве имен:
using namespace System::Runtime::InteropServices;
Не забудьте прописать lib:
#pragma comment(lib, "Advapi32.lib")
Настройки свойств при компилировании библиотеки:
Послесловие
Многие могут возразить, что подобный подход не имеет никакого смысла, и что гараздо проще в C# воспользоваться маршаллингом аргументов из стандартных библиотек. Но, на мой взгляд, мое решение является более гибким. И позволяет избавиться от несущественных переменных, подстраивая класс под себя. /Те, кто пробовал настроить DLLImport этих функций в x64 меня поймут.../
Другие могут сказать, что это вообще не имет смысла, т.к. проще было бы использовать MFC, но опять же, создавать и управлять формами в C# проще и удобнее, потому подобное решение можно считать буфферным фрэймворком, ну или стартапом подобного фрэймворка.
Спасибо за внимание.