/ src / common / ManagedCommon / Logger.cs
Logger.cs
  1  // Copyright (c) Microsoft Corporation
  2  // The Microsoft Corporation licenses this file to you under the MIT license.
  3  // See the LICENSE file in the project root for more information.
  4  
  5  using System;
  6  using System.Diagnostics;
  7  using System.Globalization;
  8  using System.IO;
  9  using System.Linq;
 10  using System.Reflection;
 11  using System.Runtime.CompilerServices;
 12  using System.Threading.Tasks;
 13  using PowerToys.Interop;
 14  
 15  namespace ManagedCommon
 16  {
 17      public static class Logger
 18      {
 19          private static readonly string Error = "Error";
 20          private static readonly string Warning = "Warning";
 21          private static readonly string Info = "Info";
 22  #if DEBUG
 23          private static readonly string Debug = "Debug";
 24  #endif
 25          private static readonly string TraceFlag = "Trace";
 26  
 27          private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "Unknown";
 28  
 29          /// <summary>
 30          /// Gets the path to the log directory for the current version of the app.
 31          /// </summary>
 32          public static string CurrentVersionLogDirectoryPath { get; private set; }
 33  
 34          /// <summary>
 35          /// Gets the path to the current log file.
 36          /// </summary>
 37          public static string CurrentLogFile { get; private set; }
 38  
 39          /// <summary>
 40          /// Gets the path to the log directory for the app.
 41          /// </summary>
 42          public static string AppLogDirectoryPath { get; private set; }
 43  
 44          /// <summary>
 45          /// Initializes the logger and sets the path for logging.
 46          /// </summary>
 47          /// <example>InitializeLogger("\\FancyZones\\Editor\\Logs")</example>
 48          /// <param name="applicationLogPath">The path to the log files folder.</param>
 49          /// <param name="isLocalLow">If the process using Logger is a low-privilege process.</param>
 50          public static void InitializeLogger(string applicationLogPath, bool isLocalLow = false)
 51          {
 52              string versionedPath = LogDirectoryPath(applicationLogPath, isLocalLow);
 53              string basePath = Path.GetDirectoryName(versionedPath);
 54  
 55              if (!Directory.Exists(versionedPath))
 56              {
 57                  Directory.CreateDirectory(versionedPath);
 58              }
 59  
 60              AppLogDirectoryPath = basePath;
 61              CurrentVersionLogDirectoryPath = versionedPath;
 62  
 63              var logFile = "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log";
 64              var logFilePath = Path.Combine(versionedPath, logFile);
 65              CurrentLogFile = logFilePath;
 66  
 67              Trace.Listeners.Add(new TextWriterTraceListener(logFilePath));
 68  
 69              Trace.AutoFlush = true;
 70  
 71              // Clean up old version log folders
 72              Task.Run(() => DeleteOldVersionLogFolders(basePath, versionedPath));
 73          }
 74  
 75          public static string LogDirectoryPath(string applicationLogPath, bool isLocalLow = false)
 76          {
 77              string basePath;
 78              if (isLocalLow)
 79              {
 80                  basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath;
 81              }
 82              else
 83              {
 84                  basePath = Constants.AppDataPath() + applicationLogPath;
 85              }
 86  
 87              string versionedPath = Path.Combine(basePath, Version);
 88              return versionedPath;
 89          }
 90  
 91          /// <summary>
 92          /// Deletes old version log folders, keeping only the current version's folder.
 93          /// </summary>
 94          /// <param name="basePath">The base path to the log files folder.</param>
 95          /// <param name="currentVersionPath">The path to the current version's log folder.</param>
 96          private static void DeleteOldVersionLogFolders(string basePath, string currentVersionPath)
 97          {
 98              try
 99              {
100                  if (!Directory.Exists(basePath))
101                  {
102                      return;
103                  }
104  
105                  var dirs = Directory.GetDirectories(basePath)
106                      .Select(d => new DirectoryInfo(d))
107                      .OrderBy(d => d.CreationTime)
108                      .Where(d => !string.Equals(d.FullName, currentVersionPath, StringComparison.OrdinalIgnoreCase))
109                      .Take(3)
110                      .ToList();
111  
112                  foreach (var directory in dirs)
113                  {
114                      try
115                      {
116                          Directory.Delete(directory.FullName, true);
117                          LogInfo($"Deleted old log directory: {directory.FullName}");
118                          Task.Delay(500).Wait();
119                      }
120                      catch (Exception ex)
121                      {
122                          LogError($"Failed to delete old log directory: {directory.FullName}", ex);
123                      }
124                  }
125              }
126              catch (Exception ex)
127              {
128                  LogError("Error cleaning up old log folders", ex);
129              }
130          }
131  
132          public static void LogError(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
133          {
134              Log(message, Error, memberName, sourceFilePath, sourceLineNumber);
135          }
136  
137          public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
138          {
139              if (ex == null)
140              {
141                  Log(message, Error, memberName, sourceFilePath, sourceLineNumber);
142              }
143              else
144              {
145                  var exMessage =
146                      message + Environment.NewLine +
147                      ex.GetType() + " (" + ex.HResult + "): " + ex.Message + Environment.NewLine;
148  
149                  if (ex.InnerException != null)
150                  {
151                      exMessage +=
152                          "Inner exception: " + Environment.NewLine +
153                          ex.InnerException.GetType() + " (" + ex.InnerException.HResult + "): " + ex.InnerException.Message + Environment.NewLine;
154                  }
155  
156                  exMessage +=
157                      "Stack trace: " + Environment.NewLine +
158                      ex.StackTrace;
159  
160                  Log(exMessage, Error, memberName, sourceFilePath, sourceLineNumber);
161              }
162          }
163  
164          public static void LogWarning(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
165          {
166              Log(message, Warning, memberName, sourceFilePath, sourceLineNumber);
167          }
168  
169          public static void LogInfo(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
170          {
171              Log(message, Info, memberName, sourceFilePath, sourceLineNumber);
172          }
173  
174          public static void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
175          {
176  #if DEBUG
177              Log(message, Debug, memberName, sourceFilePath, sourceLineNumber);
178  #endif
179          }
180  
181          public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
182          {
183              Log(string.Empty, TraceFlag, memberName, sourceFilePath, sourceLineNumber);
184          }
185  
186          private static void Log(string message, string type, string memberName, string sourceFilePath, int sourceLineNumber)
187          {
188              Trace.WriteLine("[" + DateTime.Now.TimeOfDay + "] [" + type + "] " + GetCallerInfo(memberName, sourceFilePath, sourceLineNumber));
189              Trace.Indent();
190              if (message != string.Empty)
191              {
192                  Trace.WriteLine(message);
193              }
194  
195              Trace.Unindent();
196          }
197  
198          private static string GetCallerInfo(string memberName, string sourceFilePath, int sourceLineNumber)
199          {
200              string callerFileName = "Unknown";
201  
202              try
203              {
204                  string fileName = Path.GetFileName(sourceFilePath);
205                  if (!string.IsNullOrEmpty(fileName))
206                  {
207                      callerFileName = fileName;
208                  }
209              }
210              catch (Exception)
211              {
212                  callerFileName = "Unknown";
213  #if DEBUG
214                  throw;
215  #endif
216              }
217  
218              return $"{callerFileName}::{memberName}::{sourceLineNumber}";
219          }
220      }
221  }