DEV Community

jeikabu
jeikabu

Posted on • Originally published at rendered-obsolete.github.io on

Windows Service in C#

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

Enter fullscreen mode Exit fullscreen mode

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

    //...

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

Next

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

Top comments (0)