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:
- https://stackoverflow.com/questions/15771998/how-to-give-a-user-permission-to-start-and-stop-a-particular-service-using-c-sha
- https://stackoverflow.com/questions/8379697/start-windows-service-from-application-without-admin-rightc
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.
Top comments (0)