DEV Community

loading...

Windows Service in C#

jeikabu profile image jeikabu Originally published at rendered-obsolete.github.io on ・5 min read

Part of our client runs as a Windows service for a few reasons:

  • It automatically starts when Windows 10 boots
  • OS can restart it if it fails
  • It will be running even when no user is logged in
  • When required, it has elevated privileges

Other than operations requiring elevated privileges, all those reasons only exist in a production environment. During development we want the convenience of launching/debugging from Visual Studio and easy viewing of stdout/stderr, so we also want it to function as a console application.

In our codebase and documentation this program is referred to as “layer0”.

This and other Windows programming makes heavy use of PInvoke. http://pinvoke.net/ is indispensible.

The Service

The key to creating a Windows service in C# is inheriting from System.ServiceProcess.ServiceBase.

class Layer0Service : System.ServiceProcess.ServiceBase
{
    protected override void OnStart(string[] args)
    {
        base.OnStart(args);
        StartLayer0();
    }

    protected override void OnStop()
    {
        base.OnStop();
        StopLayer0();
    }
}

StartLayer0() and StopLayer0() are routines that take care of startup and shutdown and are shared by the service and console application.

The Main()

Our program entry point:

static public int Main(string[] args)
{
    // Parse args

    if (install)
    {
        return Layer0Service.InstallService();
    }
    else if (service)
    {
        // Start as Windows service when run with --service
        var service = new Layer0Service();
        System.ServiceProcess.ServiceBase.Run(service);
        return 0;
    }

    // Running as a console program
    DoStartup();

    //...

The executable has 3 modes:

  • layer0.exe --install installs the service
  • layer0.exe --service executes as the service
  • layer0.exe executes as a normal console application

Service Installation

The --install option is used to install the service:

public static int InstallService()
{
    IntPtr hSC = IntPtr.Zero;
    IntPtr hService = IntPtr.Zero;
    try
    {
        string fullPathFilename = null;
        using (var currentProc = Process.GetCurrentProcess())
        {
            fullPathFilename = currentProc.MainModule.FileName;
        }

        hSC = OpenSCManager(null, null, SCM_ACCESS.SC_MANAGER_ALL_ACCESS);

        hService = CreateService(hSC, ShortServiceName, DisplayName,
            SERVICE_ACCESS.SERVICE_ALL_ACCESS, SERVICE_TYPE.SERVICE_WIN32_OWN_PROCESS, SERVICE_START.SERVICE_AUTO_START, SERVICE_ERROR.SERVICE_ERROR_NORMAL,
            // Start layer0 exe with --service arg
            fullPathFilename + " --service",
            null, null, null, null, null
            );

        setPermissions();

        return 0;
    }
    catch (Exception ex)
    {
        // Error handling
        return -1;
    }
    finally
    {
        if (hService != IntPtr.Zero)
            CloseServiceHandle(hService);
        if (hSC != IntPtr.Zero)
            CloseServiceHandle(hSC);
    }
}

We first get the path and name of the current executable (layer0.exe).

CreateService() is the main call to create a service. SERVICE_AUTO_START means the service will start automatically. The application specifies itself along with the --service command line argument.

This places it in services.msc where Start executes layer0.exe --service:

setPermissions()

Our platform being non-critical to the system, ordinary users should be able to start/stop it.

Reference these Stack Overflow issues:

var serviceControl = new ServiceController(ShortServiceName);
var psd = new byte[0];
uint bufSizeNeeded;
bool ok = QueryServiceObjectSecurity(serviceControl.ServiceHandle, System.Security.AccessControl.SecurityInfos.DiscretionaryAcl, psd, 0, out bufSizeNeeded);
if (!ok)
{
    int err = Marshal.GetLastWin32Error();
    if (err == 0 || err == (int)ErrorCode.ERROR_INSUFFICIENT_BUFFER)
    {
        // Resize buffer and try again
        psd = new byte[bufSizeNeeded];
        ok = QueryServiceObjectSecurity(serviceControl.ServiceHandle, System.Security.AccessControl.SecurityInfos.DiscretionaryAcl, psd, bufSizeNeeded, out bufSizeNeeded);
    }
}
if (!ok)
{
    Log("Failed to GET service permissions", Logging.LogLevel.Warn);
}

// Give permission to control service to "interactive user" (anyone logged-in to desktop)
var rsd = new RawSecurityDescriptor(psd, 0);
var dacl = new DiscretionaryAcl(false, false, rsd.DiscretionaryAcl);
//var sid = new SecurityIdentifier("D:(A;;RP;;;IU)");
var sid = new SecurityIdentifier(WellKnownSidType.InteractiveSid, null);
dacl.AddAccess(AccessControlType.Allow, sid, (int)SERVICE_ACCESS.SERVICE_ALL_ACCESS, InheritanceFlags.None, PropagationFlags.None);

// Convert discretionary ACL to raw form
var rawDacl = new byte[dacl.BinaryLength];
dacl.GetBinaryForm(rawDacl, 0);
rsd.DiscretionaryAcl = new RawAcl(rawDacl, 0);
var rawSd = new byte[rsd.BinaryLength];
rsd.GetBinaryForm(rawSd, 0);

// Set raw security descriptor on service
ok = SetServiceObjectSecurity(serviceControl.ServiceHandle, SecurityInfos.DiscretionaryAcl, rawSd);
if (!ok)
{
    Log("Failed to SET service permissions", Logging.LogLevel.Warn);
}

Failure Actions

Failure actions specify what happens when the service fails. This can be accessed from services.msc by right-clicking the service then Properties->Recovery :

This is a particularly nasty bit of pinvoke. Blame falls squarely on the function to change service configuration parameters, ChangeServiceConfig2(), because its second parameter specifies what type the third parameter is a pointer to an array of.

We heavily consulted and munged together the following sources:

public static void SetServiceRecoveryActions(IntPtr hService, params SC_ACTION[] actions)
{
    // RebootComputer requires SE_SHUTDOWN_NAME privilege
    bool needsShutdownPrivileges = actions.Any(action => action.Type == SC_ACTION_TYPE.RebootComputer);
    if (needsShutdownPrivileges)
    {
        GrantShutdownPrivilege();
    }

    var sizeofSC_ACTION = Marshal.SizeOf(typeof(SC_ACTION));
    IntPtr lpsaActions = IntPtr.Zero;
    IntPtr lpInfo = IntPtr.Zero;
    try
    {
        // Setup array of actions
        lpsaActions = Marshal.AllocHGlobal(sizeofSC_ACTION * actions.Length);
        var ptr = lpsaActions.ToInt64();
        foreach (var action in actions)
        {
            Marshal.StructureToPtr(action, (IntPtr)ptr, false);
            ptr += sizeofSC_ACTION;
        }

        // Configuration parameters
        var serviceFailureActions = new SERVICE_FAILURE_ACTIONS
        {
            dwResetPeriod = (int)TimeSpan.FromDays(1).TotalSeconds,
            lpRebootMsg = null,
            lpCommand = null,
            cActions = actions.Length,
            lpsaActions = lpsaActions,
        };
        lpInfo = Marshal.AllocHGlobal(Marshal.SizeOf(serviceFailureActions));
        Marshal.StructureToPtr(serviceFailureActions, lpInfo, false);

        if (!ChangeServiceConfig2(hService, InfoLevel.SERVICE_CONFIG_FAILURE_ACTIONS, lpInfo))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }
    }
    finally
    {
        if (lpsaActions != IntPtr.Zero)
            Marshal.FreeHGlobal(lpsaActions);
        if (lpInfo != IntPtr.Zero)
            Marshal.FreeHGlobal(lpInfo);
    }
}

The majority of this is setting up a SERVICE_FAILURE_ACTIONS for the call to ChangeServiceConfig2(). As mentioned in its documentation, if the service controller handles SC_ACTION_TYPE.RebootComputer the caller must have SE_SHUTDOWN_NAME privilege. This is fulfilled by GrantShutdownPrivilege().

Using this function we can set failure actions programmatically:

SetServiceRecoveryActions(hService,
    new SC_ACTION { Type = SC_ACTION_TYPE.RestartService, Delay = oneMinuteInMs },
    new SC_ACTION { Type = SC_ACTION_TYPE.RebootComputer, Delay = oneMinuteInMs },
    new SC_ACTION { Type = SC_ACTION_TYPE.None, Delay = 0 }
    );

GrantShutdownPrivilege() is pretty much taken verbatim from MSDN code:

static void GrantShutdownPrivilege()
{
    IntPtr hToken = IntPtr.Zero;
    try
    {
        // Open the access token associated with the current process.
        var desiredAccess = System.Security.Principal.TokenAccessLevels.AdjustPrivileges | System.Security.Principal.TokenAccessLevels.Query;
        if (!OpenProcessToken(System.Diagnostics.Process.GetCurrentProcess().Handle, (uint)desiredAccess, out hToken))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }
        // Retrieve the locally unique identifier (LUID) for the specified privilege.
        var luid = new LUID();
        if (!LookupPrivilegeValue(null, SE_SHUTDOWN_NAME, ref luid))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        TOKEN_PRIVILEGE tokenPrivilege;
        tokenPrivilege.PrivilegeCount = 1;
        tokenPrivilege.Privileges.Luid = luid;
        tokenPrivilege.Privileges.Attributes = SE_PRIVILEGE_ENABLED;

        // Enable privilege in specified access token.
        if (!AdjustTokenPrivilege(hToken, false, ref tokenPrivilege))
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }
    }
    finally
    {
        if (hToken != IntPtr.Zero)
            CloseHandle(hToken);
    }
}

As an alternative to the pinvoke nightmare, this can also be done with sc.exe:

sc.exe failure Layer0 actions= restart/60000/restart/60000/""/60000 reset= 86400

Next

We’ve got our Windows service running. Now we need to have it do something useful.

Discussion

pic
Editor guide