99using System . IO ;
1010using System . IO . MemoryMappedFiles ;
1111using System . Linq ;
12+ using System . Runtime . InteropServices ;
1213using System . Threading ;
1314using Humanizer ;
1415using Humanizer . Localisation ;
@@ -50,14 +51,15 @@ ConcurrentDictionary<string, Diagnostic> Diagnostics
5051 /// <returns>The removed diagnostic, or <see langword="null" /> if none was previously pushed.</returns>
5152 public void ReportOnce ( Action < Diagnostic > report , string product = Funding . Product )
5253 {
53- if ( Diagnostics . TryRemove ( product , out var diagnostic ) )
54+ if ( Diagnostics . TryRemove ( product , out var diagnostic ) &&
55+ GetStatus ( diagnostic ) != SponsorStatus . Grace )
5456 {
5557 // Ensure only one such diagnostic is reported per product for the entire process,
5658 // so that we can avoid polluting the error list with duplicates across multiple projects.
5759 var id = string . Concat ( Process . GetCurrentProcess ( ) . Id , product , diagnostic . Id ) ;
5860 using var mutex = new Mutex ( false , "mutex" + id ) ;
5961 mutex . WaitOne ( ) ;
60- using var mmf = MemoryMappedFile . CreateOrOpen ( id , 1 ) ;
62+ using var mmf = CreateOrOpenMemoryMappedFile ( id , 1 ) ;
6163 using var accessor = mmf . CreateViewAccessor ( ) ;
6264 if ( accessor . ReadByte ( 0 ) == 0 )
6365 {
@@ -75,52 +77,61 @@ public void ReportOnce(Action<Diagnostic> report, string product = Funding.Produ
7577 /// https://github.com/dotnet/roslyn/blob/main/docs/analyzers/Analyzer%20Actions%20Semantics.md under Ordering of actions).
7678 /// </summary>
7779 /// <returns>Optional <see cref="SponsorStatus"/> that was reported, if any.</returns>
80+ /// <devdoc>
81+ /// The SponsorLinkAnalyzer.GetOrSetStatus uses diagnostic properties to store the
82+ /// kind of diagnostic as a simple string instead of the enum. We do this so that
83+ /// multiple analyzers or versions even across multiple products, which all would
84+ /// have their own enum, can still share the same diagnostic kind.
85+ /// </devdoc>
7886 public SponsorStatus ? GetStatus ( )
79- {
80- // NOTE: the SponsorLinkAnalyzer.SetStatus uses diagnostic properties to store the
81- // kind of diagnostic as a simple string instead of the enum. We do this so that
82- // multiple analyzers or versions even across multiple products, which all would
83- // have their own enum, can still share the same diagnostic kind.
84- if ( Diagnostics . TryGetValue ( Funding . Product , out var diagnostic ) &&
85- diagnostic . Properties . TryGetValue ( nameof ( SponsorStatus ) , out var value ) )
86- {
87- // Switch on value matching DiagnosticKind names
88- return value switch
89- {
90- nameof ( SponsorStatus . Unknown ) => SponsorStatus . Unknown ,
91- nameof ( SponsorStatus . Sponsor ) => SponsorStatus . Sponsor ,
92- nameof ( SponsorStatus . Expiring ) => SponsorStatus . Expiring ,
93- nameof ( SponsorStatus . Expired ) => SponsorStatus . Expired ,
94- _ => null ,
95- } ;
96- }
97-
98- return null ;
99- }
87+ => Diagnostics . TryGetValue ( Funding . Product , out var diagnostic ) ? GetStatus ( diagnostic ) : null ;
10088
10189 /// <summary>
10290 /// Gets the status of the <see cref="Funding.Product"/>, or sets it from
10391 /// the given set of <paramref name="manifests"/> if not already set.
10492 /// </summary>
105- public SponsorStatus GetOrSetStatus ( ImmutableArray < AdditionalText > manifests )
106- => GetOrSetStatus ( ( ) => manifests ) ;
93+ public SponsorStatus GetOrSetStatus ( ImmutableArray < AdditionalText > manifests , AnalyzerConfigOptionsProvider options )
94+ => GetOrSetStatus ( ( ) => manifests , ( ) => options . GlobalOptions ) ;
10795
10896 /// <summary>
10997 /// Gets the status of the <see cref="Funding.Product"/>, or sets it from
11098 /// the given analyzer <paramref name="options"/> if not already set.
11199 /// </summary>
112100 public SponsorStatus GetOrSetStatus ( Func < AnalyzerOptions ? > options )
113- => GetOrSetStatus ( ( ) => options ( ) . GetSponsorManifests ( ) ) ;
101+ => GetOrSetStatus ( ( ) => options ( ) . GetSponsorAdditionalFiles ( ) , ( ) => options ( ) ? . AnalyzerConfigOptionsProvider . GlobalOptions ) ;
114102
115- SponsorStatus GetOrSetStatus ( Func < ImmutableArray < AdditionalText > > getManifests )
103+ SponsorStatus GetOrSetStatus ( Func < ImmutableArray < AdditionalText > > getAdditionalFiles , Func < AnalyzerConfigOptions ? > getGlobalOptions )
116104 {
117105 if ( GetStatus ( ) is { } status )
118106 return status ;
119107
120- if ( ! SponsorLink . TryRead ( out var claims , getManifests ( ) . Select ( text =>
108+ if ( ! SponsorLink . TryRead ( out var claims , getAdditionalFiles ( ) . Where ( x => x . Path . EndsWith ( ".jwt" ) ) . Select ( text =>
121109 ( text . GetText ( ) ? . ToString ( ) ?? "" , Sponsorables [ Path . GetFileNameWithoutExtension ( text . Path ) ] ) ) ) ||
122110 claims . GetExpiration ( ) is not DateTime exp )
123111 {
112+ var noGrace = getGlobalOptions ( ) is { } globalOptions &&
113+ globalOptions . TryGetValue ( "build_property.SponsorLinkNoInstallGrace" , out var value ) &&
114+ bool . TryParse ( value , out var skipCheck ) && skipCheck ;
115+
116+ if ( noGrace != true )
117+ {
118+ // Consider grace period if we can find the install time.
119+ var installed = getAdditionalFiles ( )
120+ . Where ( x => x . Path . EndsWith ( ".dll" ) )
121+ . Select ( x => File . GetLastWriteTime ( x . Path ) )
122+ . OrderByDescending ( x => x )
123+ . FirstOrDefault ( ) ;
124+
125+ if ( installed != default && ( ( DateTime . Now - installed ) . TotalDays <= Funding . Grace ) )
126+ {
127+ // report unknown, either unparsed manifest or one with no expiration (which we never emit).
128+ Push ( Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Unknown ] , null ,
129+ properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Grace ) ) ,
130+ Funding . Product , Sponsorables . Keys . Humanize ( Resources . Or ) ) ) ;
131+ return SponsorStatus . Grace ;
132+ }
133+ }
134+
124135 // report unknown, either unparsed manifest or one with no expiration (which we never emit).
125136 Push ( Diagnostic . Create ( KnownDescriptors [ SponsorStatus . Unknown ] , null ,
126137 properties : ImmutableDictionary . Create < string , string ? > ( ) . Add ( nameof ( SponsorStatus ) , nameof ( SponsorStatus . Unknown ) ) ,
@@ -169,7 +180,7 @@ Diagnostic Push(Diagnostic diagnostic, string product = Funding.Product)
169180 var id = string . Concat ( Process . GetCurrentProcess ( ) . Id , product , diagnostic . Id ) ;
170181 using var mutex = new Mutex ( false , "mutex" + id ) ;
171182 mutex . WaitOne ( ) ;
172- using var mmf = MemoryMappedFile . CreateOrOpen ( id , 1 ) ;
183+ using var mmf = CreateOrOpenMemoryMappedFile ( id , 1 ) ;
173184 using var accessor = mmf . CreateViewAccessor ( ) ;
174185 accessor . Write ( 0 , 0 ) ;
175186 Tracing . Trace ( $ "👉{ diagnostic . Severity . ToString ( ) . ToLowerInvariant ( ) } :{ Process . GetCurrentProcess ( ) . Id } :{ Process . GetCurrentProcess ( ) . ProcessName } :{ product } :{ diagnostic . Id } ") ;
@@ -178,16 +189,46 @@ Diagnostic Push(Diagnostic diagnostic, string product = Funding.Product)
178189 return diagnostic ;
179190 }
180191
192+ SponsorStatus ? GetStatus ( Diagnostic ? diagnostic ) => diagnostic ? . Properties . TryGetValue ( nameof ( SponsorStatus ) , out var value ) == true
193+ ? value switch
194+ {
195+ nameof ( SponsorStatus . Grace ) => SponsorStatus . Grace ,
196+ nameof ( SponsorStatus . Unknown ) => SponsorStatus . Unknown ,
197+ nameof ( SponsorStatus . Sponsor ) => SponsorStatus . Sponsor ,
198+ nameof ( SponsorStatus . Expiring ) => SponsorStatus . Expiring ,
199+ nameof ( SponsorStatus . Expired ) => SponsorStatus . Expired ,
200+ _ => null ,
201+ }
202+ : null ;
203+
204+ static MemoryMappedFile CreateOrOpenMemoryMappedFile ( string mapName , long capacity )
205+ {
206+ if ( RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) )
207+ {
208+ return MemoryMappedFile . CreateOrOpen ( mapName , capacity ) ;
209+ }
210+ else
211+ {
212+ // On Linux, use a file-based memory-mapped file
213+ string filePath = $ "/tmp/{ mapName } ";
214+ using ( var fs = new FileStream ( filePath , FileMode . OpenOrCreate , FileAccess . ReadWrite , FileShare . ReadWrite ) )
215+ {
216+ fs . SetLength ( capacity ) ;
217+ return MemoryMappedFile . CreateFromFile ( fs , mapName , capacity , MemoryMappedFileAccess . ReadWrite , HandleInheritability . None , false ) ;
218+ }
219+ }
220+ }
221+
181222 internal static DiagnosticDescriptor CreateSponsor ( string [ ] sponsorable , string prefix ) => new (
182- $ "{ prefix } 100",
183- Resources . Sponsor_Title ,
184- Resources . Sponsor_Message ,
185- "SponsorLink" ,
186- DiagnosticSeverity . Info ,
187- isEnabledByDefault : true ,
188- description : Resources . Sponsor_Description ,
189- helpLinkUri : "https://github.com/devlooped#sponsorlink" ,
190- "DoesNotSupportF1Help" ) ;
223+ $ "{ prefix } 100",
224+ Resources . Sponsor_Title ,
225+ Resources . Sponsor_Message ,
226+ "SponsorLink" ,
227+ DiagnosticSeverity . Info ,
228+ isEnabledByDefault : true ,
229+ description : Resources . Sponsor_Description ,
230+ helpLinkUri : "https://github.com/devlooped#sponsorlink" ,
231+ "DoesNotSupportF1Help" ) ;
191232
192233 internal static DiagnosticDescriptor CreateUnknown ( string [ ] sponsorable , string product , string prefix ) => new (
193234 $ "{ prefix } 101",
0 commit comments