diff --git a/Plugins/Wox.Plugin.Program/Logger/ProgramLogger.cs b/Plugins/Wox.Plugin.Program/Logger/ProgramLogger.cs new file mode 100644 index 0000000000..c8a104b101 --- /dev/null +++ b/Plugins/Wox.Plugin.Program/Logger/ProgramLogger.cs @@ -0,0 +1,122 @@ +using NLog; +using NLog.Config; +using NLog.Targets; +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Security; +using Wox.Infrastructure; + +namespace Wox.Plugin.Program.Logger +{ + /// + /// The Program plugin has seen many issues recorded in the Wox repo related to various loading of Windows programs. + /// This is a dedicated logger for this Program plugin with the aim to output a more friendlier message and clearer + /// log that will allow debugging to be quicker and easier. + /// + internal static class ProgramLogger + { + public const string DirectoryName = "Logs"; + + static ProgramLogger() + { + var path = Path.Combine(Constant.DataDirectory, DirectoryName, Constant.Version); + if (!Directory.Exists(path)) + { + Directory.CreateDirectory(path); + } + + var configuration = new LoggingConfiguration(); + var target = new FileTarget(); + configuration.AddTarget("file", target); + target.FileName = path.Replace(@"\", "/") + "/${shortdate}.txt"; +#if DEBUG + var rule = new LoggingRule("*", LogLevel.Debug, target); +#else + var rule = new LoggingRule("*", LogLevel.Error, target); +#endif + configuration.LoggingRules.Add(rule); + LogManager.Configuration = configuration; + } + + /// + /// Please follow exception format: |class name|calling method name|loading program path|user friendly message that explains the error + /// => Example: |Win32|LnkProgram|c:\..\chrome.exe|Permission denied on directory, but Wox should continue + /// + [MethodImpl(MethodImplOptions.Synchronized)] + internal static void LogException(string message, Exception e) + { + //Index 0 is always empty. + var parts = message.Split('|'); + var classname = parts[1]; + var callingMethodName = parts[2]; + var loadingProgramPath = parts[3]; + var interpretationMessage = parts[4]; + + Debug.WriteLine($"ERROR{message}"); + + var logger = LogManager.GetLogger(""); + + var innerExceptionNumber = 1; + + var possibleResolution = "Not yet known"; + var errorStatus = "UNKNOWN"; + + logger.Error("------------- BEGIN Wox.Plugin.Program exception -------------"); + + do + { + if (IsKnownWinProgramError(e, callingMethodName) || IsKnownUWPProgramError(e, callingMethodName)) + { + possibleResolution = "Can be ignored and Wox should still continue, however the program may not be loaded"; + errorStatus = "KNOWN"; + } + + var calledMethod = e.TargetSite != null ? e.TargetSite.ToString() : e.StackTrace; + + calledMethod = string.IsNullOrEmpty(calledMethod) ? "Not available" : calledMethod; + + logger.Error($"\nException full name: {e.GetType().FullName}" + + $"\nError status: {errorStatus}" + + $"\nClass name: {classname}" + + $"\nCalling method: {callingMethodName}" + + $"\nProgram path: {loadingProgramPath}" + + $"\nInnerException number: {innerExceptionNumber}" + + $"\nException message: {e.Message}" + + $"\nException error type: HResult {e.HResult}" + + $"\nException thrown in called method: {calledMethod}" + + $"\nPossible interpretation of the error: {interpretationMessage}" + + $"\nPossible resolution: {possibleResolution}"); + + innerExceptionNumber++; + e = e.InnerException; + } while (e != null); + + logger.Error("------------- END Wox.Plugin.Program exception -------------"); + } + + private static bool IsKnownWinProgramError(Exception e, string callingMethodName) + { + if (e.TargetSite?.Name == "GetDescription" && callingMethodName == "LnkProgram") + return true; + + if (e is SecurityException || e is UnauthorizedAccessException || e is DirectoryNotFoundException) + return true; + + return false; + } + + private static bool IsKnownUWPProgramError(Exception e, string callingMethodName) + { + if (((e.HResult == -2147024774 || e.HResult == -2147009769) && callingMethodName == "ResourceFromPri") + || (e.HResult == -2147024894 && callingMethodName == "LogoPathFromUri")) + return true; + + if (callingMethodName == "XmlNamespaces") + return true; + + return false; + } + } +} \ No newline at end of file diff --git a/Plugins/Wox.Plugin.Program/Programs/UWP.cs b/Plugins/Wox.Plugin.Program/Programs/UWP.cs index 9a7bcccb99..360b744319 100644 --- a/Plugins/Wox.Plugin.Program/Programs/UWP.cs +++ b/Plugins/Wox.Plugin.Program/Programs/UWP.cs @@ -15,7 +15,7 @@ using Windows.Management.Deployment; using AppxPackaing; using Shell; using Wox.Infrastructure; -using Wox.Infrastructure.Logger; +using Wox.Plugin.Program.Logger; using IStream = AppxPackaing.IStream; using Rect = System.Windows.Rect; @@ -84,7 +84,8 @@ namespace Wox.Plugin.Program.Programs else { var e = Marshal.GetExceptionForHR((int)hResult); - Log.Exception($"|UWP.InitializeAppInfo|SHCreateStreamOnFileEx on path <{path}> failed with HResult <{hResult}> and location <{Location}>.", e); + ProgramLogger.LogException($"|UWP|InitializeAppInfo|{path}" + + "|Error caused while trying to get the details of the UWP program", e); } } @@ -108,7 +109,9 @@ namespace Wox.Plugin.Program.Programs } else { - Log.Error($"|UWP.XmlNamespaces|can't find namespaces for <{path}>"); + ProgramLogger.LogException($"|UWP|XmlNamespaces|{path}" + + $"|Error occured while trying to get the XML from {path}", new ArgumentNullException()); + return new string[] { }; } } @@ -131,15 +134,13 @@ namespace Wox.Plugin.Program.Programs } } - Log.Error($"|UWP.InitPackageVersion| Unknown Appmanifest version UWP <{FullName}> with location <{Location}>."); + ProgramLogger.LogException($"|UWP|XmlNamespaces|{Location}" + + "|Trying to get the package version of the UWP program, but a unknown UWP appmanifest version " + + $"{FullName} from location {Location} is returned.", new FormatException()); + Version = PackageVersion.Unknown; } - - - - - public static Application[] All() { var windows10 = new Version(10, 0); @@ -153,11 +154,20 @@ namespace Wox.Plugin.Program.Programs { u = new UWP(p); } +#if !DEBUG catch (Exception e) { - Log.Exception($"|UWP.All|Can't convert Package to UWP for <{p.Id.FullName}>:", e); + ProgramLogger.LogException("|UWP|All|An unexpected error occured and " + + $"unable to convert Package to UWP for {p.Id.FullName}", e); return new Application[] { }; } +#endif +#if DEBUG //make developer aware and implement handling + catch(Exception) + { + throw; + } +#endif return u.Apps; }).ToArray(); @@ -186,18 +196,12 @@ namespace Wox.Plugin.Program.Programs ps = ps.Where(p => { bool valid; - try - { - var f = p.IsFramework; - var d = p.IsDevelopmentMode; - var path = p.InstalledLocation.Path; - valid = !f && !d && !string.IsNullOrEmpty(path); - } - catch (Exception e) - { - Log.Exception($"|UWP.CurrentUserPackages|Can't get package info for <{p.Id.FullName}>", e); - valid = false; - } + + var f = p.IsFramework; + var d = p.IsDevelopmentMode; + var path = p.InstalledLocation.Path; + valid = !f && !d && !string.IsNullOrEmpty(path); + return valid; }); return ps; @@ -382,7 +386,8 @@ namespace Wox.Plugin.Program.Programs } else { - Log.Error($"|UWP.ResourceFromPri|Can't load null or empty result pri <{source}> with uwp location <{Package.Location}>."); + ProgramLogger.LogException($"|UWP|ResourceFromPri|{Package.Location}|Can't load null or empty result " + + $"pri {source} in uwp location {Package.Location}", new NullReferenceException()); return string.Empty; } } @@ -395,7 +400,7 @@ namespace Wox.Plugin.Program.Programs // Microsoft.MicrosoftOfficeHub_17.7608.23501.0_x64__8wekyb3d8bbwe: ms-resource://Microsoft.MicrosoftOfficeHub/officehubintl/AppManifest_GetOffice_Description // Microsoft.BingFoodAndDrink_3.0.4.336_x64__8wekyb3d8bbwe: ms-resource:AppDescription var e = Marshal.GetExceptionForHR((int)hResult); - Log.Exception($"|UWP.ResourceFromPri|Load pri failed <{source}> with HResult <{hResult}> and location <{Package.Location}>.", e); + ProgramLogger.LogException($"|UWP|ResourceFromPri|{Package.Location}|Load pri failed {source} with HResult {hResult} and location {Package.Location}", e); return string.Empty; } } @@ -474,13 +479,16 @@ namespace Wox.Plugin.Program.Programs } else { - Log.Error($"|UWP.LogoPathFromUri| <{UserModelId}> can't find logo uri for <{uri}>, Package location <{Package.Location}>."); + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" + + $"|{UserModelId} can't find logo uri for {uri} in package location: {Package.Location}", new FileNotFoundException()); return string.Empty; } } else { - Log.Error($"|UWP.LogoPathFromUri| <{UserModelId}> cantains can't find extension for <{uri}> Package location <{Package.Location}>."); + ProgramLogger.LogException($"|UWP|LogoPathFromUri|{Package.Location}" + + $"|Unable to find extension from {uri} for {UserModelId} " + + $"in package location {Package.Location}", new FileNotFoundException()); return string.Empty; } } @@ -506,7 +514,9 @@ namespace Wox.Plugin.Program.Programs } else { - Log.Error($"|UWP.ImageFromPath|Can't get logo for <{UserModelId}> with path <{path}> and location <{Package.Location}>"); + ProgramLogger.LogException($"|UWP|ImageFromPath|{path}" + + $"|Unable to get logo for {UserModelId} from {path} and" + + $" located in {Package.Location}", new FileNotFoundException()); return new BitmapImage(new Uri(Constant.ErrorIcon)); } } @@ -553,7 +563,10 @@ namespace Wox.Plugin.Program.Programs } else { - Log.Error($"|UWP.PlatedImage| Can't convert background string <{BackgroundColor}> to color for <{Package.Location}>."); + ProgramLogger.LogException($"|UWP|PlatedImage|{Package.Location}" + + $"|Unable to convert background string {BackgroundColor} " + + $"to color for {Package.Location}", new InvalidOperationException()); + return new BitmapImage(new Uri(Constant.ErrorIcon)); } } diff --git a/Plugins/Wox.Plugin.Program/Programs/Win32.cs b/Plugins/Wox.Plugin.Program/Programs/Win32.cs index 250d7d4f7f..dc11a56654 100644 --- a/Plugins/Wox.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Wox.Plugin.Program/Programs/Win32.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; @@ -10,7 +9,7 @@ using System.Text; using Microsoft.Win32; using Shell; using Wox.Infrastructure; -using Wox.Infrastructure.Logger; +using Wox.Plugin.Program.Logger; namespace Wox.Plugin.Program.Programs { @@ -125,18 +124,28 @@ namespace Wox.Plugin.Program.Programs private static Win32 Win32Program(string path) { - var p = new Win32 + try { - Name = Path.GetFileNameWithoutExtension(path), - IcoPath = path, - FullPath = path, - UniqueIdentifier = path, - ParentDirectory = Directory.GetParent(path).FullName, - Description = string.Empty, - Valid = true, - Enabled = true - }; - return p; + var p = new Win32 + { + Name = Path.GetFileNameWithoutExtension(path), + IcoPath = path, + FullPath = path, + UniqueIdentifier = path, + ParentDirectory = Directory.GetParent(path).FullName, + Description = string.Empty, + Valid = true, + Enabled = true + }; + return p; + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + ProgramLogger.LogException($"|Win32|Win32Program|{path}" + + $"|Permission denied when trying to load the program from {path}", e); + + return new Win32() { Valid = false, Enabled = false }; + } } private static Win32 LnkProgram(string path) @@ -184,27 +193,43 @@ namespace Wox.Plugin.Program.Programs catch (COMException e) { // C:\\ProgramData\\Microsoft\\Windows\\Start Menu\\Programs\\MiracastView.lnk always cause exception - Log.Exception($"|Win32.LnkProgram|COMException when parsing shortcut <{path}> with HResult <{e.HResult}>", e); + ProgramLogger.LogException($"|Win32|LnkProgram|{path}"+ + "|Error caused likely due to trying to get the description of the program", e); + program.Valid = false; return program; } +#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. catch (Exception e) { - Log.Exception($"|Win32.LnkProgram|Exception when parsing shortcut <{path}>", e); + ProgramLogger.LogException($"|Win32|LnkProgram|{path}" + + "|An unexpected error occurred in the calling method LnkProgram", e); + program.Valid = false; return program; } +#endif } private static Win32 ExeProgram(string path) { - var program = Win32Program(path); - var info = FileVersionInfo.GetVersionInfo(path); - if (!string.IsNullOrEmpty(info.FileDescription)) + try { - program.Description = info.FileDescription; + var program = Win32Program(path); + var info = FileVersionInfo.GetVersionInfo(path); + if (!string.IsNullOrEmpty(info.FileDescription)) + { + program.Description = info.FileDescription; + } + return program; + } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + ProgramLogger.LogException($"|Win32|ExeProgram|{path}" + + $"|Permission denied when trying to load the program from {path}", e); + + return new Win32() { Valid = false, Enabled = false }; } - return program; } private static IEnumerable ProgramPaths(string directory, string[] suffixes) @@ -227,14 +252,15 @@ namespace Wox.Plugin.Program.Programs } catch (DirectoryNotFoundException e) { - Log.Exception($"|Program.Win32.ProgramPaths|skip directory(<{currentDirectory}>)", e); - continue; + ProgramLogger.LogException($"|Win32|ProgramPaths|{currentDirectory}" + + "|The directory trying to load the program from does not exist", e); } } } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { - Log.Exception($"|Program.Win32.ProgramPaths|Don't have permission on <{currentDirectory}>", e); + ProgramLogger.LogException($"|Win32|ProgramPaths|{currentDirectory}" + + $"|Permission denied when trying to load programs from {currentDirectory}", e); } try @@ -246,7 +272,8 @@ namespace Wox.Plugin.Program.Programs } catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) { - Log.Exception($"|Program.Win32.ProgramPaths|Don't have permission on <{currentDirectory}>", e); + ProgramLogger.LogException($"|Win32|ProgramPaths|{currentDirectory}" + + $"|Permission denied when trying to load programs from {currentDirectory}", e); } } while (folderQueue.Any()); return files; @@ -348,21 +375,30 @@ namespace Wox.Plugin.Program.Programs private static string GetProgramPathFromRegistrySubKeys(RegistryKey root, string subkey) { var path = string.Empty; - - using (var key = root.OpenSubKey(subkey)) + try { - if (key == null) + using (var key = root.OpenSubKey(subkey)) + { + if (key == null) + return string.Empty; + + var defaultValue = string.Empty; + path = key.GetValue(defaultValue) as string; + } + + if (string.IsNullOrEmpty(path)) return string.Empty; - var defaultValue = string.Empty; - path = key.GetValue(defaultValue) as string; + // fix path like this: ""\"C:\\folder\\executable.exe\"" + return path = path.Trim('"', ' '); } + catch (Exception e) when (e is SecurityException || e is UnauthorizedAccessException) + { + ProgramLogger.LogException($"|Win32|GetProgramPathFromRegistrySubKeys|{path}" + + $"|Permission denied when trying to load the program from {path}", e); - if (string.IsNullOrEmpty(path)) return string.Empty; - - // fix path like this: ""\"C:\\folder\\executable.exe\"" - return path = path.Trim('"', ' '); + } } private static Win32 GetProgramFromPath(string path) @@ -383,23 +419,41 @@ namespace Wox.Plugin.Program.Programs public static Win32[] All(Settings settings) { - var programs = new List().AsParallel(); - - var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes); - programs = programs.Concat(unregistered); - if (settings.EnableRegistrySource) + try { - var appPaths = AppPathsPrograms(settings.ProgramSuffixes); - programs = programs.Concat(appPaths); - } + var programs = new List().AsParallel(); - if (settings.EnableStartMenuSource) + var unregistered = UnregisteredPrograms(settings.ProgramSources, settings.ProgramSuffixes); + programs = programs.Concat(unregistered); + if (settings.EnableRegistrySource) + { + var appPaths = AppPathsPrograms(settings.ProgramSuffixes); + programs = programs.Concat(appPaths); + } + + if (settings.EnableStartMenuSource) + { + var startMenu = StartMenuPrograms(settings.ProgramSuffixes); + programs = programs.Concat(startMenu); + } + + return programs.ToArray(); + } +#if DEBUG //This is to make developer aware of any unhandled exception and add in handling. + catch (Exception e) { - var startMenu = StartMenuPrograms(settings.ProgramSuffixes); - programs = programs.Concat(startMenu); + throw e; } +#endif - return programs.ToArray(); +#if !DEBUG //Only do a catch all in production. + catch (Exception e) + { + ProgramLogger.LogException("|Win32|All|Not available|An unexpected error occurred", e); + + return new Win32[0]; + } +#endif } } } diff --git a/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj b/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj index 58b93f8c65..850f972ca4 100644 --- a/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj +++ b/Plugins/Wox.Plugin.Program/Wox.Plugin.Program.csproj @@ -49,6 +49,9 @@ ..\..\packages\Newtonsoft.Json.9.0.1\lib\net45\Newtonsoft.Json.dll True + + ..\..\packages\NLog.4.2.0\lib\net45\NLog.dll + @@ -71,6 +74,7 @@ AddProgramSource.xaml + diff --git a/Plugins/Wox.Plugin.Program/packages.config b/Plugins/Wox.Plugin.Program/packages.config index 79ae18b0f3..01771679cc 100644 --- a/Plugins/Wox.Plugin.Program/packages.config +++ b/Plugins/Wox.Plugin.Program/packages.config @@ -2,6 +2,7 @@ + \ No newline at end of file