Files
UnrealEngineUWP/Engine/Source/Programs/AutomationTool/Log.cs
Wes Hunt 012e45b913 UBT Utils.cs (New logging system)
* Allows us to use built-in Trace providers (console, file, etc) directly and still use our custom formatting.
* Fat comments explaining why Trace.WriteXXX functions should not be used directly in our system.
* Fixes thread safety by using Trace.WriteXXX under the hood after formatting, which uses a global lock (except on Mono, where a bug appears to be preventing this. Simulating the call on that platform).
* No need for TraceEvent overloads, which saves us the extra parameter cruft.
* Removed non-varargs overloads of Log functions (technically a bit slower, but these are already small messages).
* No longer needed VerbosityFilter and ConsoleListener classes.
* Avoid calling GetSource() if we aren't outputting the source.
* Avoid formatting the string if it won't pass the verbosity level.
* Consolidated all of UAT and UBT options into this class, so they could fully share the implementation.

UBT BuildConfiguration.cs
* Added LogFilename (and --log=<file> arg) that enables logging to a file.
* Added static ctor guard that asserts if someone tries to read a config before we have loaded config files and parsed config-override commandlines. It's a poor man's hack, but better than nothing!

UBT UEBuildConfiguration.cs
* Same static ctor guard as above.

UBT UnrealBuildTools.cs (initialization refactoring)
* In general I tried to de-mystify some of the rationale behind our startup code via fat comments.
* Broke main into 3 stages:
1. "early code" that should not try to read a config value.
  * Very little code here. Mostly setting the current directory.
  * Does an early init of logging to ensure logging is around, but config values won't be ready.
2. "Init Configuration code" that loads config files and parses command lines that may override them.
  * I isolated two locations in startup that parsed long sets of switches and moved ones that trivially affected BuildConfiguration and UEBuildConfiguration in here. Those two locations seemed to have mostly copies of the same switches, indicating serious param parsing issues at some point in time.
  * This allows switches to override config files more easily than the patchwork of re-parsing that was currently used (particularly for -verbose).
  * I did a cursory examination of later code that indicated this double (actually, triple) parsing was no longer necessary with the refactors above. Any insight into why things may have ended up this way would be helpful.
3. "Post Init code" that is actually the meat of UBT.
  * I left this code largely untouched.
  * Removed 2 of 3 different command line logging statements.
  * Removed two redundant parses of config overrides (ParseBuildConfigurationFlags).
* Guarded all of main in a try/catch block to ensure no exceptions can leak from UBT without returning a valid error code. It ALMOST already did this, but only covered the part surrounded by the Mutex.
* There was a perplexing bit that redundantly called XmlConfigLoader.Reset<> (line 683) that I struggled to understand. It turns out UEBuildConfiguration was sensitive to the current directory being set before files were loaded, and the old code called XmlConfigLoader.Init() super early, which required it to be called again after the current directory was set (see UEBuldConfiguration.UEThirdPartySourceDirectory for the cause). After my changes, I verified as best I could that these calls are no longer needed and removed them.

XmlConfigLoader.cs
* Add support for Properties in XmlConfigLoader.

AutomationTool Program.cs
* Guard logging shutdown code in try/finally so it can't be missed.

AutomationTool Log.cs
* Uses new logging system from UBT
* Removed unnecessary classes (VerbosityFilter, AutomationConsoleTraceListener, and AutomationFileTraceListener)
* Console trace logic is handled by UBT code now, moved UTF8Output handling to InitLogging.
* A custom TraceListener for file logging was unnecessary.
  * Logic to handle creating the log file and retry loops was move into InitLogging, and the result passed to a regular TextFileTraceListener.
  * Logic to handle copying the log on shutdown was moved to a ShutdownLogging function.
#codereview:robert.manuszewski,michael.trepka,kellan.carr

[CL 2526245 by Wes Hunt in Main branch]
2015-04-26 18:19:28 -04:00

267 lines
9.1 KiB
C#

// 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;
/// <summary>
/// Initializes trace logging.
/// </summary>
/// <param name="CommandLine">Command line.</param>
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
}
}
/// <summary>
/// Copies log file to the final log folder, does multiple attempts if the destination file could not be created.
/// </summary>
/// <param name="SourceFilename"></param>
/// <param name="DestFilename"></param>
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);
}
/// <summary>
/// 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.
/// </summary>
/// <returns>The newly created TraceListener, or null if it could not be created.</returns>
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;
}
/// <summary>
/// Dumps exception info to log.
/// </summary>
/// <param name="Verbosity">Verbosity</param>
/// <param name="Ex">Exception</param>
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;
}
/// <summary>
/// Returns a unique logfile name.
/// </summary>
/// <param name="Base">Base name for the logfile</param>
/// <returns>Unique logfile name.</returns>
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<string> Lines;
if (Filename == null)
{
Lines = new List<string>(AutomationMemoryLogListener.GetAccumulatedLog().Split(new string[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries));
}
else
{
Lines = new List<string>(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
/// <summary>
/// Trace console listener.
/// </summary>
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; } }
/// <summary>
/// Writes a formatted line to the console.
/// </summary>
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
}