Introduction
I would to introduce an extension of the .NET (4.0 or above) ServiceController class. Its main purpose is to add a couple of features useful to the day by day programing practice, but unfortunately not provided by the framework itself.
The features introduced by the ExtendedServiceController
class are:
- Windows services local and remote startup mode configuration
- Windows services local and remote existence check
- Windows services remote management by a proper connection and authentication flow
Background
Adding functionalities to the ServiceController
class may seems simple. Unfortunately, that class hides its whole internal logic into private
methods and properties, so we can't use them and interact directly with the services and SCM instance handles.
However, to reach my goal, I used three ingredients:
Using them, I was able to add to my extension class the features I wanted, respecting the Microsoft consistency checks as well.
About the Code
The project structure is pretty simple. It is divided into four classes:
DllNames
: It is a container for Windows DLL names used by the other classes to do P-Invokes on some Win32 API functions.
NetworkConnectionHelper
: It is a wrapper for the WNet API (WNetAddConnection2A
, WNetCancelConnection2A
) with the addition of some high level public
function used by the ExtendedServiceController
class.
WindowsServicesHelper
: It is a wrapper for the Microsoft Windows Services APIs (ChangeServiceConfig
, QueryServiceConfig
, CloseServiceHandle
) used by the ExtendedServiceController
class.
And finally, the only class you have to deal with: ExtendedServiceController
. Now, we will see how to extend the stock ServiceController class, and how to solve the private inheritance problems.
How to Get/Set the Service Startup Mode
Changing a Windows Service startup mode is not a trivial task if you are familiar with any native language for the Win32 platform. We choose the C++ for our example, so the task of changing the service startup mode may be accomplished by the following few lines of code:
BOOL ChangeServiceStartupMode(
LPCTSTR lpszMachineName,
LPCTSTR lpszServiceName,
DWORD dwStartupMode
)
{
SC_HANDLE hSCM;
SC_HANDLE hService;
hSCM = OpenSCManager(lpszMachineName, NULL, SC_MANAGER_ALL_ACCESS);
if (NULL == hSCM)
{
return false;
}
hService = OpenService(hSCM, lpszServiceName, SERVICE_CHANGE_CONFIG);
if (hService == NULL)
{
CloseServiceHandle(hSCM);
return false;
}
if (!ChangeServiceConfig(
hService,
SERVICE_NO_CHANGE,
dwStartupMode,
SERVICE_NO_CHANGE,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL
))
{
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
return false;
}
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
return true;
}
The parameter dwStartType
can assume one of the following values:
SERVICE_AUTO_START 0x00000002
SERVICE_BOOT_START 0x00000000
SERVICE_DEMAND_START 0x00000003
SERVICE_DISABLED 0x00000004
SERVICE_SYSTEM_START 0x00000001
Its counterpart is the startup mode query procedure:
BOOL QueryServiceStartupMode(
LPCTSTR lpszMachineName,
LPCTSTR lpszServiceName,
DWORD& dwStartupMode
)
{
SC_HANDLE hSCM;
SC_HANDLE hService;
LPQUERY_SERVICE_CONFIG lpServiceConfig;
DWORD dwBytesNeeded;
DWORD cbBufSize;
DWORD dwError;
hSCM = OpenSCManager(lpszMachineName, NULL, SC_MANAGER_ALL_ACCESS);
if (NULL == hSCM)
{
return false;
}
hService = OpenService(hSCM, lpszServiceName, SERVICE_QUERY_CONFIG);
if (hService == NULL)
{
CloseServiceHandle(hSCM);
return false;
}
if( !QueryServiceConfig(hService, NULL, 0, &dwBytesNeeded))
{
dwError = GetLastError();
if (ERROR_INSUFFICIENT_BUFFER == dwError)
{
cbBufSize = dwBytesNeeded;
lpServiceConfig = (LPQUERY_SERVICE_CONFIG) LocalAlloc(LMEM_FIXED, cbBufSize);
}
else
{
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
return false;
}
}
if (!QueryServiceConfig(hService, lpServiceConfig, cbBufSize, &dwBytesNeeded))
{
CloseServiceHandle(hService);
CloseServiceHandle(hSCM);
return false;
}
dwStartupMode = lpServiceConfig->dwStartType;
LocalFree(lpServiceConfig);
return true;
}
Now we have to translate the above code in C# and integrate it in the ExtendedServiceController
inherited from the ServiceController
class.
Unfortunately, that task is not so trivial because the ServiceController
class manages itself the SCM database and service handles, also they are private and thus hidden to any inherited class members.
To solve this problem, I had to decompile the ServiceController
class and replicate the behaviour of some properties and methods by the reflection framework services.
In order to publish methods and properties to access the ancestor private
members, I wrote some generics helper methods:
private FieldInfo GetPrivateFieldInfo(string sFieldName)
{
Type oType = typeof(ServiceController);
FieldInfo oFieldInfo = oType.GetField(
sFieldName,
(BindingFlags.Instance | BindingFlags.NonPublic)
);
return oFieldInfo;
}
private T GetPrivateField<T>(string sFieldName)
{
FieldInfo oFieldInfo = GetPrivateFieldInfo(sFieldName);
Debug.Assert(null != oFieldInfo);
return (T)oFieldInfo.GetValue(this);
}
private MethodInfo GetPrivateMethodInfo(string sMethodName, bool bStatic)
{
Type oType = typeof(ServiceController);
MethodInfo oMethodInfo = oType.GetMethod(
sMethodName,
(BindingFlags.NonPublic | (bStatic ? BindingFlags.Static : BindingFlags.Instance))
);
return oMethodInfo;
}
Using the above helpers I re-published some hidden properties and method that the ServiceController
class uses to control the states of the service's handles.
Here are the ones we need to extend our service controller:
private bool BrowseGranted
{
get
{
return GetPrivateField<bool>("browseGranted");
}
set
{
SetPrivateField("browseGranted", value);
}
}
By the above property, we can check if the service controller has already got the browsing (query) rights.
private bool ControlGranted
{
get
{
return GetPrivateField<bool>("controlGranted");
}
set
{
SetPrivateField("controlGranted", value);
}
}
By the above property, we can check if the service controller has already got the control (start/stop/configure) rights.
private Win32Exception CreateSafeWin32Exception()
{
MethodInfo oMethodInfo = GetPrivateMethodInfo("CreateSafeWin32Exception", true);
Debug.Assert(null != oMethodInfo);
return (Win32Exception)oMethodInfo.Invoke(this, null);
}
By the above method, the ExtendedServiceController
class can raise a Win32 exception respecting some constraints enforced by the ancestor class. It is used by most of the ServiceController
class private
methods.
private IntPtr GetServiceHandle(uint nDesiredAccess)
{
MethodInfo oMethodInfo = GetPrivateMethodInfo("GetServiceHandle", false);
Debug.Assert(null != oMethodInfo);
return (IntPtr)oMethodInfo.Invoke(this, new object[] { (int)nDesiredAccess });
}
The above method is the most important private
method of the ServiceController
class. Given the desired access mode (SERVICE_QUERY_CONFIG
or SERVICE_CHANGE_CONFIG
), by this method we can get a valid and safe handle to be passed to the QueryServiceConfig
and ChangeServiceConfig
methods. Behind the scenes, it calls the Win32 APIs OpenSCManager
and OpenService
.
Now we have all the ingredients to write the accessor methods for the property StartMode
:
public ServiceStartMode StartMode
{
get
{
return GetStartMode();
}
set
{
SetStartMode(value);
}
}
Getting the Service Startup Mode
Here, I used a helper method to check the browse service permission:
private void CheckBrowsePermission()
{
if (!BrowseGranted)
{
new ServiceControllerPermission(
ServiceControllerPermissionAccess.Browse,
MachineName,
ServiceName
).Demand();
BrowseGranted = true;
}
}
and two state instance fields:
private ServiceStartMode m_eStartMode;
private bool m_bStartModeAvailable;
The getter method, as you can see, is quite similar to the C++ implementation, we got the needed handles by the private
(safe) ServiceController
method.
private ServiceStartMode GetStartMode()
{
if (m_bStartModeAvailable)
{
return m_eStartMode;
}
CheckBrowsePermission();
IntPtr oServiceHandle = GetServiceHandle(WindowsServicesHelper.SERVICE_QUERY_CONFIG);
try
{
int nBytesNeeded;
if (!WindowsServicesHelper.QueryServiceConfig(
oServiceHandle,
IntPtr.Zero,
0,
out nBytesNeeded
))
{
int nLastWin32Error = Marshal.GetLastWin32Error();
if (WindowsServicesHelper.ERROR_INSUFFICIENT_BUFFER != nLastWin32Error)
{
throw CreateSafeWin32Exception();
}
IntPtr oConfigPointer = Marshal.AllocHGlobal(nBytesNeeded);
try
{
if (!WindowsServicesHelper.QueryServiceConfig(
oServiceHandle,
oConfigPointer,
nBytesNeeded,
out nBytesNeeded
))
{
throw CreateSafeWin32Exception();
}
WindowsServicesHelper.QUERY_SERVICE_CONFIG oQueryServiceConfig =
new WindowsServicesHelper.QUERY_SERVICE_CONFIG();
Marshal.PtrToStructure(oConfigPointer, oQueryServiceConfig);
m_eStartMode = (ServiceStartMode)oQueryServiceConfig.dwStartType;
m_bStartModeAvailable = true;
}
finally
{
Marshal.FreeHGlobal(oConfigPointer);
}
}
return m_eStartMode;
}
finally
{
WindowsServicesHelper.CloseServiceHandle(oServiceHandle);
}
}
Its important to remember that the QUERY_SERVICE_CONFIG struct
has some unsafe members, so you have to compile your project with "Allow unsafe code".
Setting the Service Startup Mode
Here I used a helper method too, to check the service control permission:
private void CheckControlPermission()
{
if (!ControlGranted)
{
new ServiceControllerPermission(
ServiceControllerPermissionAccess.Control,
MachineName,
ServiceName
).Demand();
ControlGranted = true;
}
}
Here is the setter method implementation:
private void SetStartMode(ServiceStartMode eStartMode)
{
if (m_bStartModeAvailable && (eStartMode == m_eStartMode))
{
return;
}
CheckControlPermission();
IntPtr oServiceHandle = GetServiceHandle(
WindowsServicesHelper.SERVICE_QUERY_CONFIG
| WindowsServicesHelper.SERVICE_CHANGE_CONFIG
);
try
{
if (!WindowsServicesHelper.ChangeServiceConfig(
oServiceHandle,
WindowsServicesHelper.SERVICE_NO_CHANGE,
(int)eStartMode,
WindowsServicesHelper.SERVICE_NO_CHANGE,
null,
null,
IntPtr.Zero,
null,
null,
null,
null
))
{
throw CreateSafeWin32Exception();
}
m_eStartMode = eStartMode;
m_bStartModeAvailable = true;
}
finally
{
WindowsServicesHelper.CloseServiceHandle(oServiceHandle);
}
}
Also for this method, the implementation is much like the C++ counterpart.
How to Check the Service Existence
Checking if a service (local or remote) exists or not is trivial, we can write a method based upon the ancestor private
method GenerateStatus
.
Here is the declaration of the property and its getter method:
public bool Exists
{
get
{
return GetExists();
}
}
private void GenerateStatus()
{
MethodInfo oMethodInfo = GetPrivateMethodInfo("GenerateStatus", false);
Debug.Assert(null != oMethodInfo);
oMethodInfo.Invoke(this, null);
}
private bool GetExists()
{
if (m_bExistsAvailable)
{
return m_bExists;
}
try
{
GenerateStatus();
m_bExists = true;
}
catch
{
m_bExists = false;
}
m_bExistsAvailable = true;
return m_bExists;
}
How to Manage a Remote Service
The ServiceController
class can address a remote service by one of its overloaded contructors passing to it the machine name or the IP address.
In this scenario, all works well if our application is running in an authenticated context against the target remote machine (the machines are in the same domain or they share the same user name and password), and the logged on user has the required permissions to access the remote SCM (Service Control Manager).
If one or more of the above requirements are not matched, the ServiceController
class method calls will fail!
How do we solve that problem? Exactly as we should do by the Windows command shell. We should establish an authenticated network session toward the target machine, and then we should manage the remote service.
Please look at the following example:
C:\> net use /user:<USER-NAME> \\<MACHINE-NAME> <PASSWORD>
C:\> sc.exe \\<MACHINE-NAME> stop <SERVICE-NAME>
C:\> net use \\<MACHINE-NAME> /delete
To accomplish that task in C# using native Win32 API, we have to create a connection to the IPC$ share of the target machine by the WNetAddConnection2A
function, then invoke the service controller methods, and finally revoke the connection by the WNetCancelConnection2A
function.
That flow has been wrapped by these two methods (many thanks also to aejw for the article Map Network Drive (API)):
public bool ConnectedToRemoteMachine
{
get
{
return !string.IsNullOrWhiteSpace(m_sRemoteResource);
}
}
public void ConnectToRemoteMachine(string sUserName, string sPassword)
{
if (!ConnectedToRemoteMachine)
{
NetworkConnectionHelper oNetworkConnectionHelper = new NetworkConnectionHelper();
string sRemoteResource =
string.Format(@"\\{0}\IPC$", MachineName);
if (!oNetworkConnectionHelper.AddConnection(
sRemoteResource,
sUserName,
sPassword
))
{
throw CreateSafeWin32Exception();
}
m_sRemoteResource = sRemoteResource;
}
}
public void DisconnectFromRemoteMachine()
{
if (ConnectedToRemoteMachine)
{
NetworkConnectionHelper oNetworkConnectionHelper = new NetworkConnectionHelper();
bool bResult =
oNetworkConnectionHelper.CancelConnection(m_sRemoteResource);
m_sRemoteResource = null;
if (!bResult)
{
throw CreateSafeWin32Exception();
}
}
}
So, if you have to manage a remote service, you should simply connect to the target by a valid and authorized user, then use the service controller.
In order to release the network session, please remember to disconnect the service controller.
ExtendedServiceController in Action
Finally, I would show you a self commenting example about the extended feature of the ExtendedServiceController
class.
static void Main()
{
#region Environment setup
const string sMachineName = null;
const string sServiceName = "RemoteRegistry";
const string sUserName =
"Administrator"; const string sPassword = "<PASSWORD>";
Library.ExtendedServiceController oExtendedServiceController =
new Library.ExtendedServiceController(
sServiceName,
(sMachineName ?? ".")
);
#endregion
try
{
if (!string.IsNullOrWhiteSpace(sMachineName))
{
oExtendedServiceController.ConnectToRemoteMachine(sUserName, sPassword);
}
if (!oExtendedServiceController.Exists)
{
return;
}
if (ServiceControllerStatus.Running == oExtendedServiceController.Status)
{
oExtendedServiceController.Stop();
}
if (ServiceStartMode.Automatic != oExtendedServiceController.StartMode)
{
oExtendedServiceController.StartMode = ServiceStartMode.Automatic;
}
oExtendedServiceController.Start();
if (oExtendedServiceController.ConnectedToRemoteMachine)
{
oExtendedServiceController.DisconnectFromRemoteMachine();
}
}
catch (Exception oException)
{
Console.WriteLine(oException.Message);
}
finally
{
Console.WriteLine("Hit ENTER to exit...");
Console.ReadLine();
}
}
Thank you for your attention!
Credits
History
- 2 October, 2013 - First article release