Terraria ModLoader  0.11.7.8
A mod to make and play Terraria mods
Logging.cs
Go to the documentation of this file.
1 using log4net;
2 using log4net.Appender;
3 using log4net.Config;
4 using log4net.Core;
5 using log4net.Layout;
6 using System;
7 using System.Collections.Generic;
8 using System.Diagnostics;
9 using System.IO;
10 using System.Linq;
11 using System.Reflection;
12 using System.Runtime.ExceptionServices;
13 using System.Text;
14 using System.Text.RegularExpressions;
15 using System.Threading;
16 using Terraria.Localization;
17 using Terraria.ModLoader.Core;
18 using Microsoft.Xna.Framework;
19 using Terraria.ModLoader.UI;
20 
21 namespace Terraria.ModLoader
22 {
23  public static class Logging
24  {
25  public static readonly string LogDir = Path.Combine(Program.SavePath, "Logs");
26  public static readonly string LogArchiveDir = Path.Combine(LogDir, "Old");
27  public static string LogPath { get; private set; }
28 
29  internal static ILog Terraria { get; } = LogManager.GetLogger("Terraria");
30  internal static ILog tML { get; } = LogManager.GetLogger("tML");
31 
32 #if CLIENT
33  internal const string side = "client";
34 #else
35  internal const string side = "server";
36 #endif
37 
38  private static List<string> initWarnings = new List<string>();
39  internal static void Init() {
40  if (Program.LaunchParameters.ContainsKey("-build"))
41  return;
42 
43  // This is the first file we attempt to use.
44  Utils.TryCreatingDirectory(LogDir);
45 
46  ConfigureAppenders();
47 
48  tML.InfoFormat("Starting {0} {1} {2} ({3})", ModLoader.versionedName, ReLogic.OS.Platform.Current.Type, side, DateTime.Now.ToString("d"));
49  tML.InfoFormat("Running on {0} {1}", FrameworkVersion.Framework, FrameworkVersion.Version);
50  tML.InfoFormat("Executable: {0}", Assembly.GetEntryAssembly().Location);
51  tML.InfoFormat("Working Directory: {0}", Path.GetFullPath(Directory.GetCurrentDirectory()));
52  tML.InfoFormat("Launch Parameters: {0}", string.Join(" ", Program.LaunchParameters.Select(p => (p.Key + " " + p.Value).Trim())));
53 
54  if (ModCompile.DeveloperMode)
55  tML.Info("Developer mode enabled");
56 
57  foreach (var line in initWarnings)
58  tML.Warn(line);
59 
60  AppDomain.CurrentDomain.UnhandledException += (s, args) => tML.Error("Unhandled Exception", args.ExceptionObject as Exception);
61  LogFirstChanceExceptions();
62  EnablePortablePDBTraces();
63  AssemblyResolving.Init();
64  LoggingHooks.Init();
65  LogArchiver.ArchiveLogs();
66  }
67 
68  private static void ConfigureAppenders() {
69  var layout = new PatternLayout {
70  ConversionPattern = "[%d{HH:mm:ss}] [%t/%level] [%logger]: %m%n"
71  };
72  layout.ActivateOptions();
73 
74  var appenders = new List<IAppender>();
75 #if CLIENT
76  appenders.Add(new ConsoleAppender {
77  Name = "ConsoleAppender",
78  Layout = layout
79  });
80 #endif
81  appenders.Add(new DebugAppender {
82  Name = "DebugAppender",
83  Layout = layout
84  });
85 
86  var fileAppender = new FileAppender {
87  Name = "FileAppender",
88  File = LogPath = Path.Combine(LogDir, GetNewLogFile(side)),
89  AppendToFile = false,
90  Encoding = Encoding.UTF8,
91  Layout = layout
92  };
93  fileAppender.ActivateOptions();
94  appenders.Add(fileAppender);
95 
96  BasicConfigurator.Configure(appenders.ToArray());
97  }
98 
99  private static string GetNewLogFile(string baseName) {
100  var pattern = new Regex($"{baseName}(\\d*)\\.log$");
101  var existingLogs = Directory.GetFiles(LogDir).Where(s => pattern.IsMatch(Path.GetFileName(s))).ToList();
102 
103  if (!existingLogs.All(CanOpen)) {
104  int n = existingLogs.Select(s => {
105  var tok = pattern.Match(Path.GetFileName(s)).Groups[1].Value;
106  return tok.Length == 0 ? 1 : int.Parse(tok);
107  }).Max();
108  return $"{baseName}{n + 1}.log";
109  }
110 
111  foreach (var existingLog in existingLogs.OrderBy(File.GetCreationTime)) {
112  var oldExt = ".old";
113  int n = 0;
114  while (File.Exists(existingLog + oldExt))
115  oldExt = $".old{++n}";
116 
117  try {
118  File.Move(existingLog, existingLog + oldExt);
119  }
120  catch (IOException e) {
121  initWarnings.Add($"Move failed during log initialization: {existingLog} -> {Path.GetFileName(existingLog)}{oldExt}\n{e}");
122  }
123  }
124 
125  return $"{baseName}.log";
126  }
127 
128  private static bool CanOpen(string fileName) {
129  try {
130  using (new FileStream(fileName, FileMode.Append)) ;
131  return true;
132  }
133  catch (IOException) {
134  return false;
135  }
136  }
137 
138  private static void LogFirstChanceExceptions() {
140  tML.Warn("First-chance exception reporting is not implemented on Mono");
141 
142  AppDomain.CurrentDomain.FirstChanceException += FirstChanceExceptionHandler;
143  }
144 
145  private static HashSet<string> pastExceptions = new HashSet<string>();
146  internal static void ResetPastExceptions() => pastExceptions.Clear();
147 
148  private static HashSet<string> ignoreSources = new HashSet<string> {
149  "MP3Sharp"
150  };
151  public static void IgnoreExceptionSource(string source) => ignoreSources.Add(source);
152 
153  private static List<string> ignoreContents = new List<string> {
154  "System.Console.set_OutputEncoding", // when the game is launched without a console handle (client outside dev environment)
155  "Terraria.ModLoader.Core.ModCompile",
156  "Delegate.CreateDelegateNoSecurityCheck",
157  "MethodBase.GetMethodBody",
158  "Terraria.Net.Sockets.TcpSocket.Terraria.Net.Sockets.ISocket.AsyncSend", // client disconnects from server
159  "System.Diagnostics.Process.Kill", // attempt to kill non-started process when joining server
160  "Terraria.ModLoader.Core.AssemblyManager.CecilAssemblyResolver.Resolve",
161  "Terraria.ModLoader.Engine.TMLContentManager.OpenStream" // TML content manager delegating to vanilla dir
162  };
163 
164  // there are a couple of annoying messages that happen during cancellation of asynchronous downloads
165  // that have no other useful info to suppress by
166  private static List<string> ignoreMessages = new List<string> {
167  "A blocking operation was interrupted by a call to WSACancelBlockingCall", // c#.net abort for downloads
168  "The request was aborted: The request was canceled.", // System.Net.ConnectStream.IOError
169  "Object name: 'System.Net.Sockets.Socket'.", // System.Net.Sockets.Socket.BeginReceive
170  "Object name: 'System.Net.Sockets.NetworkStream'",// System.Net.Sockets.NetworkStream.UnsafeBeginWrite
171  "This operation cannot be performed on a completed asynchronous result object.", // System.Net.ContextAwareResult.get_ContextCopy()
172  "Object name: 'SslStream'.", // System.Net.Security.SslState.InternalEndProcessAuthentication
173  "Unable to load DLL 'Microsoft.DiaSymReader.Native.x86.dll'" // Roslyn
174  };
175 
176  private static List<string> ignoreThrowingMethods = new List<string> {
177  "at Terraria.Lighting.doColors_Mode", // vanilla lighting which bug randomly happens
178  "System.Threading.CancellationToken.Throw", // an operation (task) was deliberately cancelled
179  };
180 
181  public static void IgnoreExceptionContents(string source) {
182  if (!ignoreContents.Contains(source))
183  ignoreContents.Add(source);
184  }
185 
186  private static ThreadLocal<bool> handlerActive = new ThreadLocal<bool>(() => false);
187  private static Exception previousException;
188  private static void FirstChanceExceptionHandler(object sender, FirstChanceExceptionEventArgs args) {
189  if (handlerActive.Value)
190  return;
191 
192  bool oom = args.Exception is OutOfMemoryException;
193 
194  //In case of OOM, unload the Main.tile array and do immediate garbage collection.
195  //If we don't do this, there will be a big chance that this method will fail to even quit the game, due to another OOM exception being thrown.
196  if (oom) {
197  Main.tile = null;
198 
199  GC.Collect();
200  }
201 
202  try {
203  handlerActive.Value = true;
204 
205  if (!oom) {
206  if (args.Exception == previousException ||
207  args.Exception is ThreadAbortException ||
208  ignoreSources.Contains(args.Exception.Source) ||
209  ignoreMessages.Any(str => args.Exception.Message?.Contains(str) ?? false) ||
210  ignoreThrowingMethods.Any(str => args.Exception.StackTrace?.Contains(str) ?? false))
211  return;
212  }
213 
214  var stackTrace = new StackTrace(true);
215  PrettifyStackTraceSources(stackTrace.GetFrames());
216  var traceString = stackTrace.ToString();
217 
218  if (!oom && ignoreContents.Any(traceString.Contains))
219  return;
220 
221  traceString = traceString.Substring(traceString.IndexOf('\n'));
222  var exString = args.Exception.GetType() + ": " + args.Exception.Message + traceString;
223  lock (pastExceptions) {
224  if (!pastExceptions.Add(exString))
225  return;
226  }
227 
228  previousException = args.Exception;
229  var msg = args.Exception.Message + " " + Language.GetTextValue("tModLoader.RuntimeErrorSeeLogsForFullTrace", Path.GetFileName(LogPath));
230  #if CLIENT
231  if (ModCompile.activelyModding)
232  AddChatMessage(msg, Color.OrangeRed);
233  #else
234  Console.ForegroundColor = ConsoleColor.DarkMagenta;
235  Console.WriteLine(msg);
236  Console.ResetColor();
237  #endif
238  tML.Warn(Language.GetTextValue("tModLoader.RuntimeErrorSilentlyCaughtException") + '\n' + exString);
239 
240  if (oom) {
241  string error = Language.GetTextValue("tModLoader.OutOfMemory");
242  Logging.tML.Fatal(error);
243  Interface.MessageBoxShow(error);
244  Environment.Exit(1);
245  }
246  }
247  catch (Exception e) {
248  tML.Warn("FirstChanceExceptionHandler exception", e);
249  }
250  finally {
251  handlerActive.Value = false;
252  }
253  }
254 
255  // Separate method to avoid triggering Main constructor
256  private static void AddChatMessage(string msg, Color color) {
257  if (Main.gameMenu)
258  return;
259 
260  float soundVolume = Main.soundVolume;
261  Main.soundVolume = 0f;
262  Main.NewText(msg, color);
263  Main.soundVolume = soundVolume;
264  }
265 
266  private static Regex statusRegex = new Regex(@"(.+?)[: \d]*%$");
267  internal static void LogStatusChange(string oldStatusText, string newStatusText) {
268  // trim numbers and percentage to reduce log spam
269  var oldBase = statusRegex.Match(oldStatusText).Groups[1].Value;
270  var newBase = statusRegex.Match(newStatusText).Groups[1].Value;
271  if (newBase != oldBase && newBase.Length > 0)
272  LogManager.GetLogger("StatusText").Info(newBase);
273  }
274 
275  internal static void ServerConsoleLine(string msg) => ServerConsoleLine(msg, Level.Info);
276  internal static void ServerConsoleLine(string msg, Level level, Exception ex = null, ILog log = null) {
277  if (level == Level.Warn)
278  Console.ForegroundColor = ConsoleColor.Yellow;
279  else if (level == Level.Error)
280  Console.ForegroundColor = ConsoleColor.Red;
281 
282  Console.WriteLine(msg);
283  Console.ResetColor();
284 
285  (log ?? Terraria).Logger.Log(null, level, msg, ex);
286  }
287 
288  internal static readonly FieldInfo f_fileName =
289  typeof(StackFrame).GetField("strFileName", BindingFlags.Instance | BindingFlags.NonPublic) ??
290  typeof(StackFrame).GetField("fileName", BindingFlags.Instance | BindingFlags.NonPublic);
291 
292  private static readonly Assembly TerrariaAssembly = Assembly.GetExecutingAssembly();
293 
294  public static void PrettifyStackTraceSources(StackFrame[] frames) {
295  if (frames == null)
296  return;
297 
298  foreach (var frame in frames) {
299  string filename = frame.GetFileName();
300  var assembly = frame.GetMethod()?.DeclaringType?.Assembly;
301  if (filename == null || assembly == null)
302  continue;
303 
304  string trim;
305  if (AssemblyManager.GetAssemblyOwner(assembly, out var modName))
306  trim = modName;
307  else if (assembly == TerrariaAssembly)
308  trim = "tModLoader";
309  else
310  continue;
311 
312  int idx = filename.LastIndexOf(trim, StringComparison.InvariantCultureIgnoreCase);
313  if (idx > 0) {
314  filename = filename.Substring(idx);
315  f_fileName.SetValue(frame, filename);
316  }
317  }
318  }
319 
320  private static void EnablePortablePDBTraces() {
321  if (FrameworkVersion.Framework == Framework.NetFramework && FrameworkVersion.Version >= new Version(4, 7, 2))
322  Type.GetType("System.AppContextSwitches").GetField("_ignorePortablePDBsInStackTraces", BindingFlags.Static | BindingFlags.NonPublic).SetValue(null, -1);
323  }
324  }
325 }
static void LogFirstChanceExceptions()
Definition: Logging.cs:138
static void EnablePortablePDBTraces()
Definition: Logging.cs:320
static void ConfigureAppenders()
Definition: Logging.cs:68
static void PrettifyStackTraceSources(StackFrame[] frames)
Definition: Logging.cs:294
This serves as the central class which loads mods. It contains many static fields and methods related...
Definition: ModLoader.cs:28
static void IgnoreExceptionContents(string source)
Definition: Logging.cs:181
static readonly string versionedName
Definition: ModLoader.cs:41
Command can be used in server console during MP.
static Exception previousException
Definition: Logging.cs:187
static void AddChatMessage(string msg, Color color)
Definition: Logging.cs:256
static void FirstChanceExceptionHandler(object sender, FirstChanceExceptionEventArgs args)
Definition: Logging.cs:188
Sandstorm, Hell, Above surface during Eclipse, Space
static string GetNewLogFile(string baseName)
Definition: Logging.cs:99
static bool CanOpen(string fileName)
Definition: Logging.cs:128
static readonly Framework Framework