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 servicelayer0.exe --service
executes as the servicelayer0.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.