-
Notifications
You must be signed in to change notification settings - Fork 5.2k
Annotate System.Diagnostics.EventLog
with Nullable Reference Types
#119891
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Tagging subscribers to this area: @dotnet/area-system-diagnostics-eventlog |
|
||
bool createLogKey = (logKey == null); | ||
if (createLogKey) | ||
if (logKey == null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unnecessary change
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this is a hole in Roslyn’s nullability analysis, but the analyzer warns about maybe-null logKey
otherwise
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, in that case it would be clearer if we said bool createLogKey = false
, then createLogKey = true
inside the condition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great idea.
try | ||
{ | ||
key = FindSourceRegistration(source, machineName, false); | ||
key = FindSourceRegistration(source, machineName, false)!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
there might be a bug here if key
can be null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We validate that it’s not null because we make this same call on line 450 (and throw if the value turned out null
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure, the method makes a call to interop, the registry could change in the meantime. It seems like a simple fix (maybe not in this PR) to use null-conditional operator in line 470.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The code as-is definitely makes the assumption that the registry doesn’t change. Given the validation just above, key == null
here seems like an error state that we should throw in instead of tolerate. A NRE is an unpleasant version of just that, though we could certainly make the exception better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think for the annotation PR the NRE is fine
if (!string.IsNullOrEmpty(assmLocation)) | ||
{ | ||
dllPath = Path.Combine(Path.GetDirectoryName(assmLocation), AltDllName); | ||
dllPath = Path.Combine(Path.GetDirectoryName(assmLocation)!, AltDllName); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
looks like a bug, maybe we should fix this in a separate PR?
string? directoryName = Path.GetDirectoryName(assmLocation);
if (directoryName is not null)
{
dllPath = Path.Combine(directoryName, AltDllName);
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Path.GetDirectoryName
is documented to return null
if the path denotes a root directory or is null. Is it possible to make Assembly.Location
point to a root directory?
Since it seems like an impossible state to me, maybe we could Debug.Assert
on it instead of creating a branch that's hard to validate?
} | ||
|
||
public EventLogInternal(string logName, string machineName) : this(logName, machineName, "", null) | ||
public EventLogInternal(string logName, string machineName) : this(logName, machineName, "", null! /* Special case with no parent */) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is correct
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The EventLogInternal(string, string)
constructor is only called in EventLogInternal.AddListenerComponent
and stored in an LogListeningInfo
. Only the ReadHandle
member is read on this EventLogInternal
for the duration of its lifetime, until it is removed in EventLogInternal.RemoveListenerComponent
. EventLogInternal.parent
is never called.
The other "special" constructor is called on various overloads of EventLog.WriteEntry
, where it is immediately used to call WriteEvent
(a method that also doesn't touch parent
) and then disposed.
Maybe this type could use some "separation of concerns" refactoring, or maybe we should assert that parent
is not null (it's assumed to be not-null on each callsite). I'm happy to change this from a null-suppression at the ctor to some other shape.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I my opinion, the ctor should be refactored, so that parent
is left uninitialised in the "special" cases. The majority of the logic would be moved to EventLogInternal(string logName, string machineName, string source)
. So the last ctor would look like:
public EventLogInternal(string logName, string machineName, string source, EventLog parent)
: this(logName, machineName, source)
{
this.parent = parent;
}
The field can then be declared as:
[DisallowNull]
private readonly EventLog? parent;
We would need null-suppression at the call-sites though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can narrow this down even to a single constructor; added null-suppression at the call-sites like you mentioned.
} | ||
|
||
public EventLogInternal(string logName, string machineName, string source) : this(logName, machineName, source, null) | ||
public EventLogInternal(string logName, string machineName, string source) : this(logName, machineName, source, null! /* Special case with no parent */) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is correct
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto #119891 (comment)
else | ||
{ | ||
int resourceId = (int)logkey.GetValue("DisplayNameID"); | ||
int resourceId = (int)logkey.GetValue("DisplayNameID")!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we should probably handle the null case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good point.
- All other calls to
RegistryKey.GetValue
handlenull
, though they are all reference types. - The
LogDisplayName
property seems to be "best-effort", trying a bunch of different ways to get some decent display name. In other words, we likely shouldn't be throwing ifDisplayNameID
is not present.
|
||
info = new LogListeningInfo(); | ||
|
||
info = new LogListeningInfo( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
unrelated changes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This gives us definite not-null assignment.
EventLogEntry entry = GetEntryWithOldest(i); | ||
if (this.SynchronizingObject != null && this.SynchronizingObject.InvokeRequired) | ||
this.SynchronizingObject.BeginInvoke(this.onEntryWrittenHandler, new object[] { this, new EntryWrittenEventArgs(entry) }); | ||
this.SynchronizingObject.BeginInvoke(this.onEntryWrittenHandler!, new object[] { this, new EntryWrittenEventArgs(entry) }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe this should be fixed
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went into this not knowing how much leeway I have to change any behavior; I set out to faithfully represent any holes in nullability. This is definitely one of those holes, probably hidden by this nerve-racking line:
runtime/src/libraries/System.Diagnostics.EventLog/src/System/Diagnostics/EventLogInternal.cs
Lines 573 to 575 in 2b01031
catch (Exception) | |
{ | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a null check. This event is ultimately exposed with the EventLog.EntryWritten
event; handled the scenario where nobody subscribes to it.
|
||
private EventLogEntry GetEntryWithOldest(int index) | ||
{ | ||
Debug.Assert(readHandle != null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how do we know these invariants hold?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two callsites: GetEntryAtNoThrow
(which opens a readHandle
first thing) and CompletionCallback
(which calls OldestEntryNumber
's getter earlier; OldestEntryNumber
opens a readHandle
unconditionally).
The assert on cache
should be within the if (entryPos >= 0)
block, fixing that.
|
||
private int GetNextEntryPos(int pos) | ||
{ | ||
Debug.Assert(cache != null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how do we know these invariants hold?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both callsites for this method are within GetCachedEntryPos
, which early-exits if cache == null
runtime/src/libraries/System.Diagnostics.EventLog/src/System/Diagnostics/EventLogInternal.cs
Lines 769 to 777 in 2b01031
private int GetCachedEntryPos(int entryIndex) | |
{ | |
if (cache == null || (boolFlags[Flag_forwards] && entryIndex < firstCachedEntry) || | |
(!boolFlags[Flag_forwards] && entryIndex > firstCachedEntry) || firstCachedEntry == -1) | |
{ | |
// the index falls before anything we have in the cache, or the cache | |
// is not yet valid | |
return -1; | |
} |
|
||
private int GetPreviousEntryPos(int pos) | ||
{ | ||
Debug.Assert(cache != null); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
how do we know these invariants hold?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All 3 callsites for this method are within GetCachedEntryPos
, which early-exits if cache == null
runtime/src/libraries/System.Diagnostics.EventLog/src/System/Diagnostics/EventLogInternal.cs
Lines 769 to 777 in 2b01031
private int GetCachedEntryPos(int entryIndex) | |
{ | |
if (cache == null || (boolFlags[Flag_forwards] && entryIndex < firstCachedEntry) || | |
(!boolFlags[Flag_forwards] && entryIndex > firstCachedEntry) || firstCachedEntry == -1) | |
{ | |
// the index falls before anything we have in the cache, or the cache | |
// is not yet valid | |
return -1; | |
} |
info.handleOwner.Dispose(); | ||
//Unregister the thread pool wait handle | ||
info.registeredWaitHandle.Unregister(info.waitHandle); | ||
info.registeredWaitHandle?.Unregister(info.waitHandle); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just thought I'd call out the added null check here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I was being overly cautious here, registeredWaitHandle
is set in the initial AddListenerComponent
. Reverting.
strings[i] = values[i].ToString(); | ||
else | ||
strings[i] = string.Empty; | ||
strings[i] = values[i]?.ToString() ?? string.Empty; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is a change in behavior, previously strings[i]
could be null
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
strings[i]
could technically be null by virtue of object.ToString()
returning a nullable string. The original code does a null check on values[i]
and assigns string.Empty
runtime/src/libraries/System.Diagnostics.EventLog/src/System/Diagnostics/EventLogInternal.cs
Lines 1333 to 1336 in 2b01031
if (values[i] != null) | |
strings[i] = values[i].ToString(); | |
else | |
strings[i] = string.Empty; |
This change both simplifies the code and works around a potentially null-returning ToString()
method. However, It seems to me that the original devs did not want a null
value in this array.
// make sure the strings aren't too long. MSDN says each string has a limit of 32k (32768) characters, but | ||
// experimentation shows that it doesn't like anything larger than 32766 | ||
if (strings[i].Length > 32766) | ||
if (strings[i]!.Length > 32766) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
related to above, we need to decide if strings
is string?[]?
or string[]?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The original devs were overly cautious about deliberately preventing a string?[]?
scenario, as per the line just above
runtime/src/libraries/System.Diagnostics.EventLog/src/System/Diagnostics/EventLogInternal.cs
Line 1352 in 2b01031
strings[i] ??= string.Empty; |
|
||
private sealed class LogListeningInfo | ||
{ | ||
public LogListeningInfo(EventLogInternal handleOwner, AutoResetEvent waitHandle) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is adding the constructor necessary for the null annotation work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, it's this or = null!;
on each field. I prefer this approach because it works with Roslyn instead of around Roslyn.
Contributes to #90400