4
4
using System . Collections . Concurrent ;
5
5
using System . Collections . Generic ;
6
6
using System . Collections . Immutable ;
7
+ using System . Diagnostics ;
7
8
using System . Globalization ;
8
9
using System . IO ;
10
+ using System . IO . MemoryMappedFiles ;
9
11
using System . Linq ;
12
+ using System . Threading ;
10
13
using Humanizer ;
11
14
using Humanizer . Localisation ;
12
15
using Microsoft . CodeAnalysis ;
@@ -21,6 +24,8 @@ namespace Devlooped.Sponsors;
21
24
/// </summary>
22
25
class DiagnosticsManager
23
26
{
27
+ static readonly Guid appDomainDiagnosticsKey = new ( 0x8d0e2670 , 0xe6c4 , 0x45c8 , 0x81 , 0xba , 0x5a , 0x36 , 0x81 , 0xd3 , 0x65 , 0x3e ) ;
28
+
24
29
public static Dictionary < SponsorStatus , DiagnosticDescriptor > KnownDescriptors { get ; } = new ( )
25
30
{
26
31
// Requires:
@@ -36,17 +41,31 @@ class DiagnosticsManager
36
41
/// Acceses the diagnostics dictionary for the current <see cref="AppDomain"/>.
37
42
/// </summary>
38
43
ConcurrentDictionary < string , Diagnostic > Diagnostics
39
- => AppDomainDictionary . Get < ConcurrentDictionary < string , Diagnostic > > ( nameof ( Diagnostics ) ) ;
44
+ => AppDomainDictionary . Get < ConcurrentDictionary < string , Diagnostic > > ( appDomainDiagnosticsKey . ToString ( ) ) ;
40
45
41
46
/// <summary>
42
47
/// Attemps to remove a diagnostic for the given product.
43
48
/// </summary>
44
49
/// <param name="product">The product diagnostic that might have been pushed previously.</param>
45
50
/// <returns>The removed diagnostic, or <see langword="null" /> if none was previously pushed.</returns>
46
- public Diagnostic ? Pop ( string product )
51
+ public void ReportOnce ( Action < Diagnostic > report , string product = Funding . Product )
47
52
{
48
- Diagnostics . TryRemove ( product , out var diagnostic ) ;
49
- return diagnostic ;
53
+ if ( Diagnostics . TryRemove ( product , out var diagnostic ) )
54
+ {
55
+ // Ensure only one such diagnostic is reported per product for the entire process,
56
+ // so that we can avoid polluting the error list with duplicates across multiple projects.
57
+ var id = string . Concat ( Process . GetCurrentProcess ( ) . Id , product , diagnostic . Id ) ;
58
+ using var mutex = new Mutex ( false , "mutex" + id ) ;
59
+ mutex . WaitOne ( ) ;
60
+ using var mmf = MemoryMappedFile . CreateOrOpen ( id , 1 ) ;
61
+ using var accessor = mmf . CreateViewAccessor ( ) ;
62
+ if ( accessor . ReadByte ( 0 ) == 0 )
63
+ {
64
+ accessor . Write ( 0 , 1 ) ;
65
+ report ( diagnostic ) ;
66
+ Tracing . Trace ( $ "👈{ diagnostic . Severity . ToString ( ) . ToLowerInvariant ( ) } :{ Process . GetCurrentProcess ( ) . Id } :{ Process . GetCurrentProcess ( ) . ProcessName } :{ product } :{ diagnostic . Id } ") ;
67
+ }
68
+ }
50
69
}
51
70
52
71
/// <summary>
@@ -103,7 +122,7 @@ SponsorStatus GetOrSetStatus(Func<ImmutableArray<AdditionalText>> getManifests)
103
122
claims . GetExpiration ( ) is not DateTime exp )
104
123
{
105
124
// report unknown, either unparsed manifest or one with no expiration (which we never emit).
106
- Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Unknown ] , null ,
125
+ Push ( Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Unknown ] , null ,
107
126
properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Unknown ) ) ,
108
127
Funding . Product , Sponsorables . Keys . Humanize ( Resources . Or ) ) ) ;
109
128
return SponsorStatus . Unknown ;
@@ -114,22 +133,22 @@ SponsorStatus GetOrSetStatus(Func<ImmutableArray<AdditionalText>> getManifests)
114
133
if ( exp . AddDays ( Funding . Grace ) < DateTime . Now )
115
134
{
116
135
// report expiring soon
117
- Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Expiring ] , null ,
136
+ Push ( Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Expiring ] , null ,
118
137
properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Expiring ) ) ) ) ;
119
138
return SponsorStatus . Expiring ;
120
139
}
121
140
else
122
141
{
123
142
// report expired
124
- Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Expired ] , null ,
143
+ Push ( Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Expired ] , null ,
125
144
properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Expired ) ) ) ) ;
126
145
return SponsorStatus . Expired ;
127
146
}
128
147
}
129
148
else
130
149
{
131
150
// report sponsor
132
- Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Sponsor ] , null ,
151
+ Push ( Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Sponsor ] , null ,
133
152
properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Sponsor ) ) ,
134
153
Funding . Product ) ) ;
135
154
return SponsorStatus . Sponsor ;
@@ -140,11 +159,22 @@ SponsorStatus GetOrSetStatus(Func<ImmutableArray<AdditionalText>> getManifests)
140
159
/// Pushes a diagnostic for the given product.
141
160
/// </summary>
142
161
/// <returns>The same diagnostic that was pushed, for chained invocations.</returns>
143
- Diagnostic Push ( string product , Diagnostic diagnostic )
162
+ Diagnostic Push ( Diagnostic diagnostic , string product = Funding . Product )
144
163
{
145
164
// We only expect to get one warning per sponsorable+product
146
165
// combination, and first one to set wins.
147
- Diagnostics . TryAdd ( product , diagnostic ) ;
166
+ if ( Diagnostics . TryAdd ( product , diagnostic ) )
167
+ {
168
+ // Reset the process-wide flag for this diagnostic.
169
+ var id = string . Concat ( Process . GetCurrentProcess ( ) . Id , product , diagnostic . Id ) ;
170
+ using var mutex = new Mutex ( false , "mutex" + id ) ;
171
+ mutex . WaitOne ( ) ;
172
+ using var mmf = MemoryMappedFile . CreateOrOpen ( id , 1 ) ;
173
+ using var accessor = mmf . CreateViewAccessor ( ) ;
174
+ accessor . Write ( 0 , 0 ) ;
175
+ Tracing . Trace ( $ "👉{ diagnostic . Severity . ToString ( ) . ToLowerInvariant ( ) } :{ Process . GetCurrentProcess ( ) . Id } :{ Process . GetCurrentProcess ( ) . ProcessName } :{ product } :{ diagnostic . Id } ") ;
176
+ }
177
+
148
178
return diagnostic ;
149
179
}
150
180
0 commit comments