tModLoader v0.11.8.9
A mod to make and play Terraria mods
Logging.cs
Go to the documentation of this file.
1using log4net;
2using log4net.Appender;
3using log4net.Config;
4using log4net.Core;
5using log4net.Layout;
6using System;
7using System.Collections.Generic;
8using System.Diagnostics;
9using System.IO;
10using System.Linq;
11using System.Reflection;
12using System.Runtime.ExceptionServices;
13using System.Text;
14using System.Text.RegularExpressions;
15using System.Threading;
16using Terraria.Localization;
17using Terraria.ModLoader.Core;
18using Microsoft.Xna.Framework;
19using Terraria.ModLoader.UI;
20
21namespace 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
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);
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);
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}
System.Version Version
Definition: ModLoader.cs:21
static readonly Framework Framework
static List< string > ignoreMessages
Definition: Logging.cs:166
static HashSet< string > ignoreSources
Definition: Logging.cs:148
static Regex statusRegex
Definition: Logging.cs:266
static List< string > initWarnings
Definition: Logging.cs:38
static void PrettifyStackTraceSources(StackFrame[] frames)
Definition: Logging.cs:294
static List< string > ignoreContents
Definition: Logging.cs:153
static HashSet< string > pastExceptions
Definition: Logging.cs:145
static void FirstChanceExceptionHandler(object sender, FirstChanceExceptionEventArgs args)
Definition: Logging.cs:188
static void ConfigureAppenders()
Definition: Logging.cs:68
static void LogFirstChanceExceptions()
Definition: Logging.cs:138
static readonly string LogArchiveDir
Definition: Logging.cs:26
static void IgnoreExceptionContents(string source)
Definition: Logging.cs:181
static readonly Assembly TerrariaAssembly
Definition: Logging.cs:292
static string GetNewLogFile(string baseName)
Definition: Logging.cs:99
static List< string > ignoreThrowingMethods
Definition: Logging.cs:176
static string LogPath
Definition: Logging.cs:27
static ThreadLocal< bool > handlerActive
Definition: Logging.cs:186
static void EnablePortablePDBTraces()
Definition: Logging.cs:320
static Exception previousException
Definition: Logging.cs:187
static void IgnoreExceptionSource(string source)
static bool CanOpen(string fileName)
Definition: Logging.cs:128
static readonly string LogDir
Definition: Logging.cs:25
static void AddChatMessage(string msg, Color color)
Definition: Logging.cs:256
This serves as the central class which loads mods. It contains many static fields and methods related...
Definition: ModLoader.cs:29
static readonly string versionedName
Definition: ModLoader.cs:41
@ Environment
Sandstorm, Hell, Above surface during Eclipse, Space
@ Console
Command can be used in server console during MP.