dotnet/runtime

First chance exceptions and missing logging when a service isn't given a service name

ericstj opened this issue · 3 comments

See #62579 (comment)

The default behavior for ServiceBase when a ServiceName isn't specified is to use an empty string. Per https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_table_entrya

If the service is installed with the SERVICE_WIN32_OWN_PROCESS service type, this member is ignored, but cannot be NULL. This member can be an empty string ("").

The problem, however, is that later on we use ServiceName to construct a log:

_eventLog = new EventLog("Application")
{
Source = ServiceName
};

And EventLog will throw for an empty Source:

if (Source.Length == 0)
throw new ArgumentException(SR.NeedSourceToWrite);

So in this common scenario all of these calls to WriteEventLog result in first chance exceptions, and nothing is logged:

private void WriteLogEntry(string message, EventLogEntryType type = EventLogEntryType.Information)
{
// EventLog failures shouldn't affect the service operation
try
{
if (AutoLog)
{
EventLog.WriteEntry(message, type);
}
}
catch
{
// Do nothing. Not having the event log is bad, but not starting the service as a result is worse.
}
}

In this scenario we can actually acquire the service name from SCM, it's returned in the ServiceMain callback https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nc-winsvc-lpservice_main_functiona

the first argument (lpServiceArgVectors[0]) is the name of the service

I have confirmed this to be the case. So we can simply capture it here and set ServiceName rather than skipping it:

if (argCount > 0)
{
char** argsAsPtr = (char**)argPointer.ToPointer();
//Lets read the arguments
// the first arg is always the service name. We don't want to pass that in.
args = new string[argCount - 1];
for (int index = 0; index < args.Length; ++index)
{
// we increment the pointer first so we skip over the first argument.
argsAsPtr++;
args[index] = Marshal.PtrToStringUni((IntPtr)(*argsAsPtr))!;
}
}

Tagging subscribers to this area: @dotnet/area-system-serviceprocess
See info in area-owners.md if you want to be subscribed.

Issue Details

See #62579 (comment)

The default behavior for ServiceBase when a ServiceName isn't specified is to use an empty string. Per https://docs.microsoft.com/en-us/windows/win32/api/winsvc/ns-winsvc-service_table_entrya

If the service is installed with the SERVICE_WIN32_OWN_PROCESS service type, this member is ignored, but cannot be NULL. This member can be an empty string ("").

The problem, however, is that later on we use ServiceName to construct a log:

_eventLog = new EventLog("Application")
{
Source = ServiceName
};

And EventLog will throw for an empty Source:

if (Source.Length == 0)
throw new ArgumentException(SR.NeedSourceToWrite);

So in this common scenario all of these calls to WriteEventLog result in first chance exceptions, and nothing is logged:

private void WriteLogEntry(string message, EventLogEntryType type = EventLogEntryType.Information)
{
// EventLog failures shouldn't affect the service operation
try
{
if (AutoLog)
{
EventLog.WriteEntry(message, type);
}
}
catch
{
// Do nothing. Not having the event log is bad, but not starting the service as a result is worse.
}
}

In this scenario we can actually acquire the service name from SCM, it's returned in the ServiceMain callback https://docs.microsoft.com/en-us/windows/win32/api/winsvc/nc-winsvc-lpservice_main_functiona

the first argument (lpServiceArgVectors[0]) is the name of the service

I have confirmed this to be the case. So we can simply capture it here and set ServiceName rather than skipping it:

if (argCount > 0)
{
char** argsAsPtr = (char**)argPointer.ToPointer();
//Lets read the arguments
// the first arg is always the service name. We don't want to pass that in.
args = new string[argCount - 1];
for (int index = 0; index < args.Length; ++index)
{
// we increment the pointer first so we skip over the first argument.
argsAsPtr++;
args[index] = Marshal.PtrToStringUni((IntPtr)(*argsAsPtr))!;
}
}

Author: ericstj
Assignees: -
Labels:

area-System.ServiceProcess, untriaged

Milestone: -

@ericstj I see this is causing: #62579, from the discussion it seems like fixing this just luckily fixes that but the fix should be more intentional. Let me know if we want to service this, for now I'll set it as 7.0

Right, this wasn't blocking. It was just a significant influence to timing and that linked issue was a race condition sensitive to timing. I think it is fine to just fix this in 7.0.