Click here to Skip to main content
15,867,977 members
Articles / Desktop Programming / Win32

Extended Windows Service Controller

Rate me:
Please Sign up or sign in to vote.
4.89/5 (9 votes)
3 Oct 2013CPOL5 min read 30.3K   525   15   5
.NET ServiceController class extension

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:

C++
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:

C++
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:

C++
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:

C#
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:

C#
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.

C#
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.

C#
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.

C#
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:

C#
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:

C#
private void CheckBrowsePermission()
{
    if (!BrowseGranted)
    {
        new ServiceControllerPermission(
            ServiceControllerPermissionAccess.Browse, 
            MachineName, 
            ServiceName
        ).Demand();

        BrowseGranted = true;
    }
} 

and two state instance fields:

C#
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.

C#
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:

C#
private void CheckControlPermission()
{
    if (!ControlGranted)
    {
        new ServiceControllerPermission(
            ServiceControllerPermissionAccess.Control, 
            MachineName, 
            ServiceName
        ).Demand();

        ControlGranted = true;
    }
} 

Here is the setter method implementation:

C#
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:

C#
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)):

C#
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";  // or "Domain\UserName"
    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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
Italy Italy
What about me?! Hmmm... I'm a developer since I was 12 years old, and... that's all! Wink | ;)

Comments and Discussions

 
QuestionIssues in a Domain environment Pin
Dimenus8820-Feb-15 7:02
Dimenus8820-Feb-15 7:02 
AnswerRe: Issues in a Domain environment Pin
Dimenus8820-Feb-15 8:30
Dimenus8820-Feb-15 8:30 
GeneralLooks interesting Pin
ledtech33-Oct-13 8:00
ledtech33-Oct-13 8:00 
GeneralRe: Looks interesting Pin
Antonio Petricca3-Oct-13 21:04
Antonio Petricca3-Oct-13 21:04 
GeneralRe: Looks interesting Pin
ledtech34-Oct-13 4:34
ledtech34-Oct-13 4:34 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.