Skip to content

Commit e7d57d9

Browse files
authored
Merge pull request #1546 from VictoriousRaptor/ProgramIndexPATH
[Dev][Program Plugin] Optimize program indexing performance
2 parents 0484046 + 9dcf030 commit e7d57d9

File tree

3 files changed

+122
-51
lines changed

3 files changed

+122
-51
lines changed

Plugins/Flow.Launcher.Plugin.Program/Languages/en.xaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
Insert file suffixes you want to index. Suffixes should be separated by ';'. (ex>bat;py)
6262
</system:String>
6363
<system:String x:Key="flowlauncher_plugin_program_protocol_tooltip">
64-
Insert protocols of .url files you want to index. Protocols should be separated by ';'. (ex>ftp;netflix)
64+
Insert protocols of .url files you want to index. Protocols should be separated by ';', and should end with "://". (ex>ftp://;mailto://)
6565
</system:String>
6666

6767
<system:String x:Key="flowlauncher_plugin_program_run_as_different_user">Run As Different User</system:String>

Plugins/Flow.Launcher.Plugin.Program/Main.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ public async Task InitAsync(PluginInitContext context)
102102
await Task.WhenAll(a, b);
103103

104104
Win32.WatchProgramUpdate(_settings);
105-
UWP.WatchPackageChange();
105+
_ = UWP.WatchPackageChange();
106106
}
107107

108108
public static void IndexWin32Programs()

Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs

Lines changed: 120 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,19 @@ public class Win32 : IProgram, IEquatable<Win32>
2525
public string Name { get; set; }
2626
public string UniqueIdentifier { get => _uid; set => _uid = value == null ? string.Empty : value.ToLowerInvariant(); } // For path comparison
2727
public string IcoPath { get; set; }
28+
/// <summary>
29+
/// Path of the file. It's the path of .lnk or .url for .lnk and .url.
30+
/// </summary>
2831
public string FullPath { get; set; }
32+
/// <summary>
33+
/// Path of the excutable for .lnk, or the URL for .url.
34+
/// </summary>
2935
public string LnkResolvedPath { get; set; }
36+
/// <summary>
37+
/// Path of the actual executable file.
38+
/// </summary>
39+
public string ExecutablePath => LnkResolvedPath ?? FullPath;
40+
public string WorkingDir => Directory.GetParent(ExecutablePath)?.FullName ?? string.Empty;
3041
public string ParentDirectory { get; set; }
3142
public string ExecutableName { get; set; }
3243
public string Description { get; set; }
@@ -97,10 +108,23 @@ public Result Result(string query, IPublicAPI api)
97108
matchResult.MatchData = new List<int>();
98109
}
99110

111+
string subtitle = string.Empty;
112+
if (!Main._settings.HideAppsPath)
113+
{
114+
if (Extension(FullPath) == UrlExtension)
115+
{
116+
subtitle = LnkResolvedPath;
117+
}
118+
else
119+
{
120+
subtitle = FullPath;
121+
}
122+
}
123+
100124
var result = new Result
101125
{
102126
Title = title,
103-
SubTitle = Main._settings.HideAppsPath ? string.Empty : LnkResolvedPath ?? FullPath,
127+
SubTitle = subtitle,
104128
IcoPath = IcoPath,
105129
Score = matchResult.Score,
106130
TitleHighlightData = matchResult.MatchData,
@@ -116,8 +140,8 @@ public Result Result(string query, IPublicAPI api)
116140

117141
var info = new ProcessStartInfo
118142
{
119-
FileName = LnkResolvedPath ?? FullPath,
120-
WorkingDirectory = ParentDirectory,
143+
FileName = ExecutablePath,
144+
WorkingDirectory = WorkingDir,
121145
UseShellExecute = true,
122146
Verb = runAsAdmin ? "runas" : null
123147
};
@@ -143,8 +167,8 @@ public List<Result> ContextMenus(IPublicAPI api)
143167
{
144168
var info = new ProcessStartInfo
145169
{
146-
FileName = FullPath,
147-
WorkingDirectory = ParentDirectory,
170+
FileName = ExecutablePath,
171+
WorkingDirectory = WorkingDir,
148172
UseShellExecute = true
149173
};
150174

@@ -162,8 +186,8 @@ public List<Result> ContextMenus(IPublicAPI api)
162186
{
163187
var info = new ProcessStartInfo
164188
{
165-
FileName = FullPath,
166-
WorkingDirectory = ParentDirectory,
189+
FileName = ExecutablePath,
190+
WorkingDirectory = WorkingDir,
167191
Verb = "runas",
168192
UseShellExecute = true
169193
};
@@ -224,6 +248,15 @@ private static Win32 Win32Program(string path)
224248

225249
return Default;
226250
}
251+
#if !DEBUG
252+
catch (Exception e)
253+
{
254+
ProgramLogger.LogException($"|Win32|Win32Program|{path}" +
255+
"|An unexpected error occurred in the calling method Win32Program", e);
256+
257+
return Default;
258+
}
259+
#endif
227260
}
228261

229262
private static Win32 LnkProgram(string path)
@@ -241,8 +274,7 @@ private static Win32 LnkProgram(string path)
241274
var extension = Extension(target);
242275
if (extension == ExeExtension && File.Exists(target))
243276
{
244-
program.LnkResolvedPath = program.FullPath;
245-
program.FullPath = Path.GetFullPath(target).ToLowerInvariant();
277+
program.LnkResolvedPath = Path.GetFullPath(target);
246278
program.ExecutableName = Path.GetFileName(target);
247279

248280
var description = _helper.description;
@@ -270,25 +302,22 @@ private static Win32 LnkProgram(string path)
270302
"|Error caused likely due to trying to get the description of the program",
271303
e);
272304

273-
program.Valid = false;
274-
return program;
305+
return Default;
275306
}
276307
catch (FileNotFoundException e)
277308
{
278309
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
279310
"|An unexpected error occurred in the calling method LnkProgram", e);
280311

281-
program.Valid = false;
282-
return program;
312+
return Default;
283313
}
284314
#if !DEBUG //Only do a catch all in production. This is so make developer aware of any unhandled exception and add the exception handling in.
285315
catch (Exception e)
286316
{
287317
ProgramLogger.LogException($"|Win32|LnkProgram|{path}" +
288318
"|An unexpected error occurred in the calling method LnkProgram", e);
289319

290-
program.Valid = false;
291-
return program;
320+
return Default;
292321
}
293322
#endif
294323
}
@@ -342,6 +371,13 @@ private static Win32 ExeProgram(string path)
342371
program.Description = info.FileDescription;
343372
return program;
344373
}
374+
catch (FileNotFoundException e)
375+
{
376+
ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
377+
$"|File not found when trying to load the program from {path}", e);
378+
379+
return Default;
380+
}
345381
catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException)
346382
{
347383
ProgramLogger.LogException($"|Win32|ExeProgram|{path}" +
@@ -351,7 +387,7 @@ private static Win32 ExeProgram(string path)
351387
}
352388
}
353389

354-
private static IEnumerable<string> ProgramPaths(string directory, string[] suffixes, bool recursive = true)
390+
private static IEnumerable<string> EnumerateProgramsInDir(string directory, string[] suffixes, bool recursive = true)
355391
{
356392
if (!Directory.Exists(directory))
357393
return Enumerable.Empty<string>();
@@ -376,24 +412,23 @@ private static string Extension(string path)
376412
}
377413
}
378414

379-
private static IEnumerable<Win32> UnregisteredPrograms(List<ProgramSource> sources, string[] suffixes, string[] protocols)
415+
private static IEnumerable<Win32> UnregisteredPrograms(List<string> directories, string[] suffixes, string[] protocols)
380416
{
381417
// Disabled custom sources are not in DisabledProgramSources
382-
var paths = ExceptDisabledSource(sources.Where(s => Directory.Exists(s.Location) && s.Enabled)
383-
.AsParallel()
384-
.SelectMany(s => ProgramPaths(s.Location, suffixes)))
385-
.Distinct();
418+
var paths = directories.AsParallel()
419+
.SelectMany(s => EnumerateProgramsInDir(s, suffixes));
386420

387-
var programs = paths.Select(x => GetProgramFromPath(x, protocols));
421+
// Remove disabled programs in DisabledProgramSources
422+
var programs = ExceptDisabledSource(paths).Select(x => GetProgramFromPath(x, protocols));
388423
return programs;
389424
}
390425

391426
private static IEnumerable<Win32> StartMenuPrograms(string[] suffixes, string[] protocols)
392427
{
393428
var directory1 = Environment.GetFolderPath(Environment.SpecialFolder.Programs);
394429
var directory2 = Environment.GetFolderPath(Environment.SpecialFolder.CommonPrograms);
395-
var paths1 = ProgramPaths(directory1, suffixes);
396-
var paths2 = ProgramPaths(directory2, suffixes);
430+
var paths1 = EnumerateProgramsInDir(directory1, suffixes);
431+
var paths2 = EnumerateProgramsInDir(directory2, suffixes);
397432

398433
var toFilter = paths1.Concat(paths2);
399434

@@ -402,26 +437,22 @@ private static IEnumerable<Win32> StartMenuPrograms(string[] suffixes, string[]
402437
return programs;
403438
}
404439

405-
private static IEnumerable<Win32> PATHPrograms(string[] suffixes, string[] protocols)
440+
private static IEnumerable<Win32> PATHPrograms(string[] suffixes, string[] protocols, List<string> commonParents)
406441
{
407442
var pathEnv = Environment.GetEnvironmentVariable("Path");
408-
if (String.IsNullOrEmpty(pathEnv))
409-
{
410-
return Array.Empty<Win32>();
443+
if (String.IsNullOrEmpty(pathEnv))
444+
{
445+
return Array.Empty<Win32>();
411446
}
412447

413448
var paths = pathEnv.Split(";", StringSplitOptions.RemoveEmptyEntries).DistinctBy(p => p.ToLowerInvariant());
414449

415-
var toFilter = paths.AsParallel().SelectMany(p => ProgramPaths(p, suffixes, recursive: false));
450+
paths = paths.Where(x => commonParents.All(parent => !x.StartsWith(parent, StringComparison.OrdinalIgnoreCase)));
451+
452+
var toFilter = paths.AsParallel().SelectMany(p => EnumerateProgramsInDir(p, suffixes, recursive: false));
416453

417454
var programs = ExceptDisabledSource(toFilter.Distinct())
418-
.Select(x => Extension(x) switch
419-
{
420-
ShortcutExtension => LnkProgram(x),
421-
UrlExtension => UrlProgram(x, protocols),
422-
ExeExtension => ExeProgram(x),
423-
_ => Win32Program(x)
424-
});
455+
.Select(x => GetProgramFromPath(x, protocols));
425456
return programs;
426457
}
427458

@@ -496,9 +527,6 @@ private static Win32 GetProgramFromPath(string path, string[] protocols)
496527

497528
path = Environment.ExpandEnvironmentVariables(path);
498529

499-
if (!File.Exists(path))
500-
return Default;
501-
502530
return Extension(path) switch
503531
{
504532
ShortcutExtension => LnkProgram(path),
@@ -545,15 +573,15 @@ public static IEnumerable<T> DistinctBy<T, R>(IEnumerable<T> source, Func<T, R>
545573

546574
private static IEnumerable<Win32> ProgramsHasher(IEnumerable<Win32> programs)
547575
{
548-
return programs.GroupBy(p => p.FullPath.ToLowerInvariant())
576+
return programs.GroupBy(p => p.ExecutablePath.ToLowerInvariant())
549577
.AsParallel()
550578
.SelectMany(g =>
551579
{
552580
var temp = g.Where(g => !string.IsNullOrEmpty(g.Description)).ToList();
553581
if (temp.Any())
554582
return DistinctBy(temp, x => x.Description);
555583
return g.Take(1);
556-
}).ToArray();
584+
});
557585
}
558586

559587

@@ -565,11 +593,15 @@ public static Win32[] All(Settings settings)
565593
var suffixes = settings.GetSuffixes();
566594
var protocols = settings.GetProtocols();
567595

568-
var unregistered = UnregisteredPrograms(settings.ProgramSources, suffixes, protocols);
596+
// Disabled custom sources are not in DisabledProgramSources
597+
var sources = settings.ProgramSources.Where(s => Directory.Exists(s.Location) && s.Enabled).Distinct();
598+
var commonParents = GetCommonParents(sources);
599+
600+
var unregistered = UnregisteredPrograms(commonParents, suffixes, protocols);
569601

570602
programs = programs.Concat(unregistered);
571603

572-
var autoIndexPrograms = Enumerable.Empty<Win32>();
604+
var autoIndexPrograms = Enumerable.Empty<Win32>(); // for single programs, not folders
573605

574606
if (settings.EnableRegistrySource)
575607
{
@@ -585,11 +617,11 @@ public static Win32[] All(Settings settings)
585617

586618
if (settings.EnablePATHSource)
587619
{
588-
var path = PATHPrograms(settings.GetSuffixes(), protocols);
589-
autoIndexPrograms = autoIndexPrograms.Concat(path);
620+
var path = PATHPrograms(settings.GetSuffixes(), protocols, commonParents);
621+
programs = programs.Concat(path);
590622
}
591623

592-
autoIndexPrograms = ProgramsHasher(autoIndexPrograms);
624+
autoIndexPrograms = ProgramsHasher(autoIndexPrograms).ToArray();
593625

594626
return programs.Concat(autoIndexPrograms).Where(x => x.Valid).Distinct().ToArray();
595627
}
@@ -651,11 +683,13 @@ public static void WatchProgramUpdate(Settings settings)
651683
if (settings.EnableStartMenuSource)
652684
paths.AddRange(GetStartMenuPaths());
653685

654-
paths.AddRange(from source in settings.ProgramSources where source.Enabled select source.Location);
686+
var customSources = GetCommonParents(settings.ProgramSources);
687+
paths.AddRange(customSources);
655688

689+
var fileExtensionToWatch = settings.GetSuffixes();
656690
foreach (var directory in from path in paths where Directory.Exists(path) select path)
657691
{
658-
WatchDirectory(directory);
692+
WatchDirectory(directory, fileExtensionToWatch);
659693
}
660694

661695
_ = Task.Run(MonitorDirectoryChangeAsync);
@@ -676,7 +710,7 @@ public static async Task MonitorDirectoryChangeAsync()
676710
}
677711
}
678712

679-
public static void WatchDirectory(string directory)
713+
public static void WatchDirectory(string directory, string[] extensions)
680714
{
681715
if (!Directory.Exists(directory))
682716
{
@@ -688,6 +722,10 @@ public static void WatchDirectory(string directory)
688722
watcher.Deleted += static (_, _) => indexQueue.Writer.TryWrite(default);
689723
watcher.EnableRaisingEvents = true;
690724
watcher.IncludeSubdirectories = true;
725+
foreach (var extension in extensions)
726+
{
727+
watcher.Filters.Add($"*.{extension}");
728+
}
691729

692730
Watchers.Add(watcher);
693731
}
@@ -699,5 +737,38 @@ public static void Dispose()
699737
fileSystemWatcher.Dispose();
700738
}
701739
}
740+
741+
// https://stackoverflow.com/a/66877016
742+
private static bool IsSubPathOf(string subPath, string basePath)
743+
{
744+
var rel = Path.GetRelativePath(basePath, subPath);
745+
return rel != "."
746+
&& rel != ".."
747+
&& !rel.StartsWith("../")
748+
&& !rel.StartsWith(@"..\")
749+
&& !Path.IsPathRooted(rel);
750+
}
751+
752+
private static List<string> GetCommonParents(IEnumerable<ProgramSource> programSources)
753+
{
754+
// To avoid unnecessary io
755+
// like c:\windows and c:\windows\system32
756+
var grouped = programSources.GroupBy(p => p.Location.ToLowerInvariant()[0]); // group by disk
757+
List<string> result = new();
758+
foreach (var group in grouped)
759+
{
760+
HashSet<ProgramSource> parents = group.ToHashSet();
761+
foreach (var source in group)
762+
{
763+
if (parents.Any(p => IsSubPathOf(source.Location, p.Location) &&
764+
source != p))
765+
{
766+
parents.Remove(source);
767+
}
768+
}
769+
result.AddRange(parents.Select(x => x.Location));
770+
}
771+
return result.DistinctBy(x => x.ToLowerInvariant()).ToList();
772+
}
702773
}
703774
}

0 commit comments

Comments
 (0)