11using System ;
2+ using System . Collections . Concurrent ;
23using System . Collections . Generic ;
34using System . Diagnostics ;
45using System . Linq ;
@@ -13,37 +14,52 @@ namespace Flow.Launcher.Plugin.AppUpgrader
1314 public class AppUpgrader : IAsyncPlugin
1415 {
1516 internal PluginInitContext Context ;
16- private List < UpgradableApp > upgradableApps ;
17-
17+ private ConcurrentBag < UpgradableApp > upgradableApps ;
18+ private readonly SemaphoreSlim _refreshSemaphore = new SemaphoreSlim ( 1 , 1 ) ;
19+ private DateTime _lastRefreshTime = DateTime . MinValue ;
20+ private const int CACHE_EXPIRATION_MINUTES = 15 ;
21+ private const int COMMAND_TIMEOUT_SECONDS = 10 ;
22+ private static readonly Regex AppLineRegex = new Regex ( @"^(.+?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$" ,
23+ RegexOptions . Compiled | RegexOptions . IgnoreCase ) ;
1824 public Task InitAsync ( PluginInitContext context )
1925 {
2026 Context = context ;
27+ Task . Run ( async ( ) =>
28+ {
29+ try
30+ {
31+ await RefreshUpgradableAppsAsync ( ) ;
32+ }
33+ catch ( Exception ex ) { }
34+ } ) ;
35+
36+ ThreadPool . SetMinThreads ( Environment . ProcessorCount * 2 , Environment . ProcessorCount * 2 ) ;
2137 return Task . CompletedTask ;
2238 }
2339
2440 public async Task < List < Result > > QueryAsync ( Query query , CancellationToken token )
2541 {
26- var results = new List < Result > ( ) ;
27- try
28- {
29- upgradableApps = GetUpgradableAppsAsync ( ) . GetAwaiter ( ) . GetResult ( ) ;
30- }
31- catch ( Exception ex )
42+ if ( ShouldRefreshCache ( ) )
3243 {
33- return results ;
44+ await RefreshUpgradableAppsAsync ( ) ;
3445 }
3546
36- await Task . Yield ( ) ;
37- string keyword = query . FirstSearch . Trim ( ) . ToLower ( ) ;
38-
3947 if ( upgradableApps == null || ! upgradableApps . Any ( ) )
4048 {
41- return results ;
49+ return new List < Result >
50+ {
51+ new Result
52+ {
53+ Title = "No updates available" ,
54+ SubTitle = "All applications are up-to-date." ,
55+ IcoPath = "Images\\ app.png"
56+ }
57+ } ;
4258 }
4359
44- foreach ( var app in upgradableApps . ToList ( ) )
45- {
46- results . Add ( new Result
60+ return upgradableApps . AsParallel ( )
61+ . WithDegreeOfParallelism ( Environment . ProcessorCount )
62+ . Select ( app => new Result
4763 {
4864 Title = $ "Upgrade { app . Name } ",
4965 SubTitle = $ "From { app . Version } to { app . AvailableVersion } ",
@@ -55,29 +71,60 @@ public async Task<List<Result>> QueryAsync(Query query, CancellationToken token)
5571 {
5672 await PerformUpgradeAsync ( app ) ;
5773 }
58- catch ( Exception ex ) { }
59- } , token ) ;
74+ catch ( Exception ex )
75+ {
76+ Context . API . ShowMsg ( $ "Upgrade failed: { ex . Message } ") ;
77+ }
78+ } ) ;
6079 return true ;
6180 } ,
6281 IcoPath = "Images\\ app.png"
63- } ) ;
64- }
82+ } ) . ToList ( ) ;
83+ }
6584
66- return results ;
85+
86+ private bool ShouldRefreshCache ( )
87+ {
88+ return upgradableApps == null ||
89+ DateTime . Now - _lastRefreshTime > TimeSpan . FromMinutes ( CACHE_EXPIRATION_MINUTES ) ;
6790 }
6891
92+ private async Task RefreshUpgradableAppsAsync ( )
93+ {
94+ if ( ! ShouldRefreshCache ( ) )
95+ return ;
96+
97+ await _refreshSemaphore . WaitAsync ( ) ;
98+ try
99+ {
100+ if ( ! ShouldRefreshCache ( ) )
101+ return ;
102+
103+ var apps = await GetUpgradableAppsAsync ( ) ;
104+ upgradableApps = new ConcurrentBag < UpgradableApp > ( apps ) ;
105+ _lastRefreshTime = DateTime . Now ;
106+ }
107+ catch ( Exception ex ) { }
108+ finally
109+ {
110+ _refreshSemaphore . Release ( ) ;
111+ }
112+ }
69113
70114
71115 private async Task PerformUpgradeAsync ( UpgradableApp app )
72116 {
73- Context . API . ShowMsg ( $ "Attempting to update { app . Name } ...") ;
117+ Context . API . ShowMsg ( $ "Preparing to update { app . Name } ... This may take a moment .") ;
74118 await ExecuteWingetCommandAsync ( $ "winget upgrade --id { app . Id } -i") ;
75- upgradableApps = await GetUpgradableAppsAsync ( ) ;
119+ upgradableApps = new ConcurrentBag < UpgradableApp > ( upgradableApps . Where ( a => a . Id != app . Id ) ) ;
120+ await RefreshUpgradableAppsAsync ( ) ;
76121 }
77122
123+
78124 private async Task < List < UpgradableApp > > GetUpgradableAppsAsync ( )
79125 {
80- var output = await ExecuteWingetCommandAsync ( "winget upgrade" ) ;
126+ using var cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( COMMAND_TIMEOUT_SECONDS ) ) ;
127+ var output = await ExecuteWingetCommandAsync ( "winget upgrade" , cts . Token ) ;
81128 return ParseWingetOutput ( output ) ;
82129 }
83130
@@ -86,17 +133,15 @@ private List<UpgradableApp> ParseWingetOutput(string output)
86133 var upgradableApps = new List < UpgradableApp > ( ) ;
87134 var lines = output . Split ( new [ ] { '\r ' , '\n ' } , StringSplitOptions . RemoveEmptyEntries ) ;
88135
89- // Find the header line
90- int startIndex = Array . FindIndex ( lines , line => Regex . IsMatch ( line , @"^-+$" ) ) ;
136+ var startIndex = Array . FindIndex ( lines , line => Regex . IsMatch ( line , @"^-+$" ) ) ;
91137 if ( startIndex == - 1 ) return upgradableApps ;
92138
93- // Analyze each line after the header line, ignoring the last line (which is the number of upgrade available)
94139 for ( int i = startIndex + 1 ; i < lines . Length - 1 ; i ++ )
95140 {
96141 var line = lines [ i ] . Trim ( ) ;
97142 if ( string . IsNullOrWhiteSpace ( line ) ) continue ;
98143
99- var match = Regex . Match ( line , @"^(.+?)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)$" ) ;
144+ var match = AppLineRegex . Match ( line ) ;
100145 if ( match . Success )
101146 {
102147 var app = new UpgradableApp
@@ -114,11 +159,10 @@ private List<UpgradableApp> ParseWingetOutput(string output)
114159 }
115160 }
116161 }
117-
118162 return upgradableApps ;
119163 }
120164
121- private async Task < string > ExecuteWingetCommandAsync ( string command )
165+ private async Task < string > ExecuteWingetCommandAsync ( string command , CancellationToken cancellationToken = default )
122166 {
123167 var processInfo = new ProcessStartInfo ( "cmd.exe" , "/c " + command )
124168 {
@@ -128,22 +172,20 @@ private async Task<string> ExecuteWingetCommandAsync(string command)
128172 CreateNoWindow = true
129173 } ;
130174
131- var output = new StringBuilder ( ) ;
132-
133- using ( var process = new Process ( ) )
134- {
135- process . StartInfo = processInfo ;
175+ using var process = Process . Start ( processInfo ) ;
176+ if ( process == null )
177+ throw new InvalidOperationException ( "Failed to start process." ) ;
136178
137- process . OutputDataReceived += ( sender , e ) => { if ( e . Data != null ) output . AppendLine ( e . Data ) ; } ;
138- process . ErrorDataReceived += ( sender , e ) => { if ( e . Data != null ) output . AppendLine ( e . Data ) ; } ;
179+ var output = await process . StandardOutput . ReadToEndAsync ( ) ;
180+ var error = await process . StandardError . ReadToEndAsync ( ) ;
139181
140- process . Start ( ) ;
141- process . BeginOutputReadLine ( ) ;
142- process . BeginErrorReadLine ( ) ;
143- await Task . Run ( ( ) => process . WaitForExit ( ) ) ;
182+ await process . WaitForExitAsync ( cancellationToken ) ;
183+ if ( ! string . IsNullOrEmpty ( error ) )
184+ {
185+ throw new InvalidOperationException ( error ) ;
144186 }
145187
146- return output . ToString ( ) ;
188+ return output ;
147189 }
148190 }
149191
0 commit comments