// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Runtime.CompilerServices; using System.Diagnostics; namespace AutomationTool { #region LogUtils public class LogUtils { private static string LogFilename; /// /// Initializes trace logging. /// /// Command line. public static void InitLogging(string[] CommandLine) { UnrealBuildTool.Log.InitLogging( bLogTimestamps: CommandUtils.ParseParam(CommandLine, "-Timestamps"), bLogVerbose: CommandUtils.ParseParam(CommandLine, "-Verbose"), bLogSeverity: true, bLogSources: true, bColorConsoleOutput: true, TraceListeners: new TraceListener[] { new ConsoleTraceListener(), // could return null, but InitLogging handles this gracefully. CreateLogFileListener(out LogFilename), //@todo - this is only used by GUBP nodes. Ideally we don't waste this 20MB if we are not running GUBP. new AutomationMemoryLogListener(), }); // ensure UTF8Output flag is respected, since we are initializing logging early in the program. if (CommandLine.Any(Arg => Arg.Equals("-utf8output", StringComparison.InvariantCultureIgnoreCase))) { Console.OutputEncoding = new System.Text.UTF8Encoding(false, false); } } public static void ShutdownLogging() { // This closes all the output streams immediately, inside the Global Lock, so it's threadsafe. Trace.Close(); // from here we can copy the log file to its final resting place try { // Try to copy the log file to the log folder. The reason why it's done here is that // at the time the log file is being initialized the env var may not yet be set (this // applies to local log folder in particular) var LogFolder = Environment.GetEnvironmentVariable(EnvVarNames.LogFolder); if (!String.IsNullOrEmpty(LogFolder) && Directory.Exists(LogFolder) && !String.IsNullOrEmpty(LogFilename) && File.Exists(LogFilename)) { var DestFilename = CommandUtils.CombinePaths(LogFolder, "UAT_" + Path.GetFileName(LogFilename)); SafeCopyLogFile(LogFilename, DestFilename); } } catch (Exception) { // Silently ignore, logging is pointless because eveything is shut down at this point } } /// /// Copies log file to the final log folder, does multiple attempts if the destination file could not be created. /// /// /// private static void SafeCopyLogFile(string SourceFilename, string DestFilename) { const int MaxAttempts = 10; int AttemptNo = 0; var DestLogFilename = DestFilename; bool Result = false; do { try { File.Copy(SourceFilename, DestLogFilename, true); Result = true; } catch (Exception) { var ModifiedFilename = String.Format("{0}_{1}{2}", Path.GetFileNameWithoutExtension(DestFilename), AttemptNo, Path.GetExtension(DestLogFilename)); DestLogFilename = CommandUtils.CombinePaths(Path.GetDirectoryName(DestFilename), ModifiedFilename); AttemptNo++; } } while (Result == false && AttemptNo <= MaxAttempts); } /// /// Creates the TraceListener used for file logging. /// We cannot simply use a TextWriterTraceListener because we need more flexibility when the file cannot be created. /// TextWriterTraceListener lazily creates the file, silently failing when it cannot. /// /// The newly created TraceListener, or null if it could not be created. private static TraceListener CreateLogFileListener(out string LogFilename) { StreamWriter LogFile = null; const int MaxAttempts = 10; int Attempt = 0; var TempLogFolder = Path.GetTempPath(); do { if (Attempt == 0) { LogFilename = CommandUtils.CombinePaths(TempLogFolder, "Log.txt"); } else { LogFilename = CommandUtils.CombinePaths(TempLogFolder, String.Format("Log_{0}.txt", Attempt)); } try { // We do not need to set AutoFlush on the StreamWriter because we set Trace.AutoFlush, which calls it for us. // Not only would this be redundant, StreamWriter AutoFlush does not flush the encoder, while a direct call to // StreamWriter.Flush() will, which is what the Trace system with AutoFlush = true will do. // Internally, FileStream constructor opens the file with good arguments for writing to log files. return new TextWriterTraceListener(new StreamWriter(LogFilename), "AutomationFileLogListener"); } catch (Exception Ex) { if (Attempt == (MaxAttempts - 1)) { // Clear out the LogFilename to indicate we were not able to write one. LogFilename = null; UnrealBuildTool.Log.TraceWarning("Unable to create log file: {0}", LogFilename); UnrealBuildTool.Log.TraceWarning(LogUtils.FormatException(Ex)); } } } while (LogFile == null && ++Attempt < MaxAttempts); return null; } /// /// Dumps exception info to log. /// /// Verbosity /// Exception public static string FormatException(Exception Ex) { var Message = String.Format("Exception in {0}: {1}{2}Stacktrace: {3}", Ex.Source, Ex.Message, Environment.NewLine, Ex.StackTrace); if (Ex.InnerException != null) { Message += String.Format("InnerException in {0}: {1}{2}Stacktrace: {3}", Ex.InnerException.Source, Ex.InnerException.Message, Environment.NewLine, Ex.InnerException.StackTrace); } return Message; } /// /// Returns a unique logfile name. /// /// Base name for the logfile /// Unique logfile name. public static string GetUniqueLogName(string Base) { const int MaxAttempts = 1000; string LogFilename = string.Empty; int Attempt = 0; do { if (Attempt == 0) { LogFilename = String.Format("{0}.txt", Base); } else { LogFilename = String.Format("{0}.{1}.txt", Base, Attempt); } } while (File.Exists(LogFilename) && ++Attempt < MaxAttempts); if (File.Exists(LogFilename)) { throw new AutomationException(String.Format("Failed to create logfile {0}.", LogFilename)); } return LogFilename; } public static string GetLogTail(string Filename = null, int NumLines = 250) { List Lines; if (Filename == null) { Lines = new List(AutomationMemoryLogListener.GetAccumulatedLog().Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)); } else { Lines = new List(CommandUtils.ReadAllLines(Filename)); } if (Lines.Count > NumLines) { Lines.RemoveRange(0, Lines.Count - NumLines); } string Result = ""; foreach (var Line in Lines) { Result += Line + Environment.NewLine; } return Result; } } #endregion #region AutomationMemoryLogListener /// /// Trace console listener. /// class AutomationMemoryLogListener : TraceListener { private static StringBuilder AccumulatedLog = new StringBuilder(1024 * 1024 * 20); private static object SyncObject = new object(); public override bool IsThreadSafe { get { return true; } } /// /// Writes a formatted line to the console. /// private void WriteLinePrivate(string Message) { lock (SyncObject) { AccumulatedLog.AppendLine(Message); } } public static string GetAccumulatedLog() { lock (SyncObject) { return AccumulatedLog.ToString(); } } #region TraceListener Interface public override void Write(string message) { WriteLinePrivate(message); } public override void WriteLine(string message) { WriteLinePrivate(message); } #endregion } #endregion }