Skip to content

Commit a69645c

Browse files
authored
Merge pull request #297 from sharwell/default-init
Remove reliance on s_isTracking to detect recursion
2 parents a50c10a + 96b1594 commit a69645c

File tree

2 files changed

+64
-27
lines changed

2 files changed

+64
-27
lines changed

src/coverlet.core/Instrumentation/Instrumenter.cs

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ internal class Instrumenter
2929
private FieldDefinition _customTrackerHitsFilePath;
3030
private ILProcessor _customTrackerClassConstructorIl;
3131
private TypeDefinition _customTrackerTypeDef;
32+
private MethodReference _customTrackerRegisterUnloadEventsMethod;
3233
private MethodReference _customTrackerRecordHitMethod;
3334

3435
public Instrumenter(string module, string identifier, string[] excludeFilters, string[] includeFilters, string[] excludedFiles, string[] excludedAttributes)
@@ -76,6 +77,7 @@ private void InstrumentModule()
7677

7778
using (var module = ModuleDefinition.ReadModule(stream, parameters))
7879
{
80+
var containsAppContext = module.GetType(nameof(System), nameof(AppContext)) != null;
7981
var types = module.GetTypes();
8082
AddCustomModuleTrackerToModule(module);
8183

@@ -95,13 +97,49 @@ private void InstrumentModule()
9597
}
9698

9799
// Fixup the custom tracker class constructor, according to all instrumented types
100+
if (_customTrackerRegisterUnloadEventsMethod == null)
101+
{
102+
_customTrackerRegisterUnloadEventsMethod = new MethodReference(
103+
nameof(ModuleTrackerTemplate.RegisterUnloadEvents), module.TypeSystem.Void, _customTrackerTypeDef);
104+
}
105+
98106
Instruction lastInstr = _customTrackerClassConstructorIl.Body.Instructions.Last();
107+
108+
if (!containsAppContext)
109+
{
110+
// For "normal" cases, where the instrumented assembly is not the core library, we add a call to
111+
// RegisterUnloadEvents to the static constructor of the generated custom tracker. Due to static
112+
// initialization constraints, the core library is handled separately below.
113+
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Call, _customTrackerRegisterUnloadEventsMethod));
114+
}
115+
99116
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldc_I4, _result.HitCandidates.Count));
100117
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Newarr, module.TypeSystem.Int32));
101118
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsArray));
102119
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldstr, _result.HitsFilePath));
103120
_customTrackerClassConstructorIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Stsfld, _customTrackerHitsFilePath));
104121

122+
if (containsAppContext)
123+
{
124+
// Handle the core library by instrumenting System.AppContext.OnProcessExit to directly call
125+
// the UnloadModule method of the custom tracker type. This avoids loops between the static
126+
// initialization of the custom tracker and the static initialization of the hosting AppDomain
127+
// (which for the core library case will be instrumented code).
128+
var eventArgsType = new TypeReference(nameof(System), nameof(EventArgs), module, module.TypeSystem.CoreLibrary);
129+
var customTrackerUnloadModule = new MethodReference(nameof(ModuleTrackerTemplate.UnloadModule), module.TypeSystem.Void, _customTrackerTypeDef);
130+
customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(module.TypeSystem.Object));
131+
customTrackerUnloadModule.Parameters.Add(new ParameterDefinition(eventArgsType));
132+
133+
var appContextType = new TypeReference(nameof(System), nameof(AppContext), module, module.TypeSystem.CoreLibrary);
134+
var onProcessExitMethod = new MethodReference("OnProcessExit", module.TypeSystem.Void, appContextType).Resolve();
135+
var onProcessExitIl = onProcessExitMethod.Body.GetILProcessor();
136+
137+
lastInstr = onProcessExitIl.Body.Instructions.Last();
138+
onProcessExitIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldnull));
139+
onProcessExitIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Ldnull));
140+
onProcessExitIl.InsertBefore(lastInstr, Instruction.Create(OpCodes.Call, customTrackerUnloadModule));
141+
}
142+
105143
module.Write(stream);
106144
}
107145
}
@@ -135,12 +173,9 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module)
135173
{
136174
MethodDefinition methodOnCustomType = new MethodDefinition(methodDef.Name, methodDef.Attributes, methodDef.ReturnType);
137175

138-
if (methodDef.Name == "RecordHit")
176+
foreach (var parameter in methodDef.Parameters)
139177
{
140-
foreach (var parameter in methodDef.Parameters)
141-
{
142-
methodOnCustomType.Parameters.Add(new ParameterDefinition(module.ImportReference(parameter.ParameterType)));
143-
}
178+
methodOnCustomType.Parameters.Add(new ParameterDefinition(module.ImportReference(parameter.ParameterType)));
144179
}
145180

146181
foreach (var variable in methodDef.Body.Variables)
@@ -166,8 +201,11 @@ private void AddCustomModuleTrackerToModule(ModuleDefinition module)
166201
else
167202
{
168203
// Move to the custom type
169-
instr.Operand = new MethodReference(
170-
methodReference.Name, methodReference.ReturnType, _customTrackerTypeDef);
204+
var updatedMethodReference = new MethodReference(methodReference.Name, methodReference.ReturnType, _customTrackerTypeDef);
205+
foreach (var parameter in methodReference.Parameters)
206+
updatedMethodReference.Parameters.Add(new ParameterDefinition(parameter.Name, parameter.Attributes, module.ImportReference(parameter.ParameterType)));
207+
208+
instr.Operand = updatedMethodReference;
171209
}
172210
}
173211
else if (instr.Operand is FieldReference fieldReference)

src/coverlet.template/ModuleTrackerTemplate.cs

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -20,33 +20,36 @@ public static class ModuleTrackerTemplate
2020
public static string HitsFilePath;
2121
public static int[] HitsArray;
2222

23-
// Special case when instrumenting CoreLib, the static below prevents infinite loop in CoreLib
24-
// while allowing the tracker template to call any of its types and functions.
25-
private static bool s_isTracking;
26-
2723
static ModuleTrackerTemplate()
2824
{
29-
s_isTracking = true;
30-
AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule);
31-
AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule);
32-
s_isTracking = false;
33-
3425
// At the end of the instrumentation of a module, the instrumenter needs to add code here
3526
// to initialize the static fields according to the values derived from the instrumentation of
3627
// the module.
3728
}
3829

30+
// A call to this method will be injected in the static constructor above for most cases. However, if the
31+
// current assembly is System.Private.CoreLib (or more specifically, defines System.AppDomain), a call directly
32+
// to UnloadModule will be injected in System.AppContext.OnProcessExit.
33+
public static void RegisterUnloadEvents()
34+
{
35+
AppDomain.CurrentDomain.ProcessExit += new EventHandler(UnloadModule);
36+
AppDomain.CurrentDomain.DomainUnload += new EventHandler(UnloadModule);
37+
}
38+
3939
public static void RecordHit(int hitLocationIndex)
4040
{
41-
if (s_isTracking)
41+
// Make sure to avoid recording if this is a call to RecordHit within the AppDomain setup code in an
42+
// instrumented build of System.Private.CoreLib.
43+
if (HitsArray is null)
4244
return;
4345

4446
Interlocked.Increment(ref HitsArray[hitLocationIndex]);
4547
}
4648

4749
public static void UnloadModule(object sender, EventArgs e)
4850
{
49-
s_isTracking = true;
51+
// Claim the current hits array and reset it to prevent double-counting scenarios.
52+
var hitsArray = Interlocked.Exchange(ref HitsArray, new int[HitsArray.Length]);
5053

5154
// The same module can be unloaded multiple times in the same process via different app domains.
5255
// Use a global mutex to ensure no concurrent access.
@@ -61,8 +64,8 @@ public static void UnloadModule(object sender, EventArgs e)
6164
using (var fs = new FileStream(HitsFilePath, FileMode.CreateNew))
6265
using (var bw = new BinaryWriter(fs))
6366
{
64-
bw.Write(HitsArray.Length);
65-
foreach (int hitCount in HitsArray)
67+
bw.Write(hitsArray.Length);
68+
foreach (int hitCount in hitsArray)
6669
{
6770
bw.Write(hitCount);
6871
}
@@ -82,25 +85,21 @@ public static void UnloadModule(object sender, EventArgs e)
8285
using (var bw = new BinaryWriter(fs))
8386
{
8487
int hitsLength = br.ReadInt32();
85-
if (hitsLength != HitsArray.Length)
88+
if (hitsLength != hitsArray.Length)
8689
{
8790
throw new InvalidOperationException(
88-
$"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {HitsArray.Length}");
91+
$"{HitsFilePath} has {hitsLength} entries but on memory {nameof(HitsArray)} has {hitsArray.Length}");
8992
}
9093

9194
for (int i = 0; i < hitsLength; ++i)
9295
{
9396
int oldHitCount = br.ReadInt32();
9497
bw.Seek(-sizeof(int), SeekOrigin.Current);
95-
bw.Write(HitsArray[i] + oldHitCount);
98+
bw.Write(hitsArray[i] + oldHitCount);
9699
}
97100
}
98101
}
99102

100-
// Prevent any double counting scenario, i.e.: UnloadModule called twice (not sure if this can happen in practice ...)
101-
// Only an issue if DomainUnload and ProcessExit can both happens, perhaps can be removed...
102-
Array.Clear(HitsArray, 0, HitsArray.Length);
103-
104103
// On purpose this is not under a try-finally: it is better to have an exception if there was any error writing the hits file
105104
// this case is relevant when instrumenting corelib since multiple processes can be running against the same instrumented dll.
106105
mutex.ReleaseMutex();

0 commit comments

Comments
 (0)