44using System . Collections . Concurrent ;
55using System . Collections . Generic ;
66using System . Collections . Immutable ;
7+ using System . Diagnostics ;
78using System . Globalization ;
89using System . IO ;
10+ using System . IO . MemoryMappedFiles ;
911using System . Linq ;
12+ using System . Threading ;
1013using Humanizer ;
1114using Humanizer . Localisation ;
1215using Microsoft . CodeAnalysis ;
@@ -21,6 +24,8 @@ namespace Devlooped.Sponsors;
2124/// </summary>
2225class DiagnosticsManager
2326{
27+ static readonly Guid appDomainDiagnosticsKey = new ( 0x8d0e2670 , 0xe6c4 , 0x45c8 , 0x81 , 0xba , 0x5a , 0x36 , 0x81 , 0xd3 , 0x65 , 0x3e ) ;
28+
2429 public static Dictionary < SponsorStatus , DiagnosticDescriptor > KnownDescriptors { get ; } = new ( )
2530 {
2631 // Requires:
@@ -36,17 +41,31 @@ class DiagnosticsManager
3641 /// Acceses the diagnostics dictionary for the current <see cref="AppDomain"/>.
3742 /// </summary>
3843 ConcurrentDictionary < string , Diagnostic > Diagnostics
39- => AppDomainDictionary . Get < ConcurrentDictionary < string , Diagnostic > > ( nameof ( Diagnostics ) ) ;
44+ => AppDomainDictionary . Get < ConcurrentDictionary < string , Diagnostic > > ( appDomainDiagnosticsKey . ToString ( ) ) ;
4045
4146 /// <summary>
4247 /// Attemps to remove a diagnostic for the given product.
4348 /// </summary>
4449 /// <param name="product">The product diagnostic that might have been pushed previously.</param>
4550 /// <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 )
4752 {
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+ }
5069 }
5170
5271 /// <summary>
@@ -103,7 +122,7 @@ SponsorStatus GetOrSetStatus(Func<ImmutableArray<AdditionalText>> getManifests)
103122 claims . GetExpiration ( ) is not DateTime exp )
104123 {
105124 // 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 ,
107126 properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Unknown ) ) ,
108127 Funding . Product , Sponsorables . Keys . Humanize ( Resources . Or ) ) ) ;
109128 return SponsorStatus . Unknown ;
@@ -114,22 +133,22 @@ SponsorStatus GetOrSetStatus(Func<ImmutableArray<AdditionalText>> getManifests)
114133 if ( exp . AddDays ( Funding . Grace ) < DateTime . Now )
115134 {
116135 // report expiring soon
117- Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Expiring ] , null ,
136+ Push ( Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Expiring ] , null ,
118137 properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Expiring ) ) ) ) ;
119138 return SponsorStatus . Expiring ;
120139 }
121140 else
122141 {
123142 // report expired
124- Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Expired ] , null ,
143+ Push ( Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Expired ] , null ,
125144 properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Expired ) ) ) ) ;
126145 return SponsorStatus . Expired ;
127146 }
128147 }
129148 else
130149 {
131150 // report sponsor
132- Push ( Funding . Product , Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Sponsor ] , null ,
151+ Push ( Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Sponsor ] , null ,
133152 properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Sponsor ) ) ,
134153 Funding . Product ) ) ;
135154 return SponsorStatus . Sponsor ;
@@ -140,11 +159,22 @@ SponsorStatus GetOrSetStatus(Func<ImmutableArray<AdditionalText>> getManifests)
140159 /// Pushes a diagnostic for the given product.
141160 /// </summary>
142161 /// <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 )
144163 {
145164 // We only expect to get one warning per sponsorable+product
146165 // 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+
148178 return diagnostic ;
149179 }
150180
0 commit comments