Using SCM to manage drivers in C # implemented with dll in C ++ / cli

Service Control Manager (SCM)


SCM is a server implemented in Windows for remote management of services (calling procedures).

In order to run the driver in Windows, a service is assigned to it that provides control of this driver. Not to be confused with the device that creates the driver in the system through which messages are exchanged with the driver. This device is created after the driver starts, but SCM provides the driver itself in the system. Using SCM, you can: add, remove, start, or stop services.

Formulation of the problem


Write a buffer class to simplify SCM in C #.
The appearance of this class itself can be recognized very simply:

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;
};

Constructor, destructor, finalizer, main methods, from attributes only HANDLE of the SCM object. It follows that an instance of an object of this class will contain the created SCM object, and the methods simplify the work with it. The class is a buffer, and since it is implemented in C ++ / cli, it will be automatically scaled to work in the .NET environment, respectively, and in C #.

Error resolution


The main problem with this class is the return of error codes that occurred during the operation of SCM, which is desirable at the very first stage of work to be replaced with exceptions more familiar to .NET. To do this, you can create a similar class:

[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);
};

As we see, an instance of this class will contain, as an attribute, only the code number that will be received from GetLastError (). And when you try to cast an instance to the System :: String type, it displays the full text of the message description using Windows.

The class has two constructors, the first one by default: it saves an error code during execution. The second - receives an error code as an argument. The second one must be used in cases where it is necessary to throw an exception, but before that perform some action, after which the GetLastError () command will return incorrect values. To do this, an error code is saved, actions are performed, then an exception is raised. An example of such actions can be found below: clearing the PTR used for marshaling (you need to clear the PTR before throwing an exception, because you won’t be able to return to this piece of code in the future).

Implementation
KernelErrorException::KernelErrorException(void)
{
	this->errorsource = GetLastError();
}
KernelErrorException::KernelErrorException(DWORD Errorsource)
{
	this->errorsource = Errorsource;
}

Moreover, the implementation of the methods will be the most elementary:

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();
}


Memory allocated for SCM must be cleared


The second problem of working with SCM in .NET: handle SCM cannot live long, otherwise it will lead to a system crash. Therefore, when using it, it is necessary to ensure that the programmer himself does not deal with the garbage dump. You will have to strictly describe the constructor and finalizer, but in the destructor, according to the logic of the Dispose-pattern, the finalizer is called [thanks GraD_Kh ]. The finalizer describes the release of unmanage objects:

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()
{
	CloseServiceHandle(this->SCMHandle));
}

Main functionality


The implementation of all methods is very simple, its basis is the call of a specific relevant procedure, but correct execution necessarily requires all checks for exceptional situations.

Implementation
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();
}


The first method associates the sys file with the service, adding this service to the system. The second - removes the driver from the system, the other two - start and stop the service, respectively.

Examples of use in C #:


try
{
	using (ServiceControlManager scm = new ServiceControlManager())
	{
		scm.AddDriver(serviceName, filePath);
		scm.StartDriver(serviceName); 
		scm.StopDriver(serviceName);
		scm.DeleteDriver(serviceName);
	}
}
catch (Exception ex)
{
}


Compile Settings


The most important thing to remember is to constantly use marshaling between the managed and unmanaged heap. Let me remind you that for marshalling it is necessary to be in the namespace:
using namespace System::Runtime::InteropServices;


Do not forget to register lib:
#pragma comment(lib, "Advapi32.lib")


Property settings when compiling the library:
Project properties

Afterword


Many may argue that such an approach makes no sense, and that it is much easier in C # to use marshalling arguments from standard libraries. But, in my opinion, my decision is more flexible. And it allows you to get rid of non-essential variables by adjusting the class for yourself. / Those who tried to configure DLLImport of these functions in x64 will understand me ... /

GitHub library sources with the user interface program

Application window

Also popular now: