|
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using Microsoft.TestPlatform.PlatformAbstractions;
namespace Microsoft.VisualStudio.TestPlatform.ObjectModel;
/// <summary>
/// Performs logging to a file and rolls the output file when either time or size thresholds are
/// exceeded.
/// </summary>
public class RollingFileTraceListener : TextWriterTraceListener
{
private readonly int _rollSizeInBytes;
private bool _isDisposed;
/// <summary>
/// Initializes a new instance of the <see cref="RollingFileTraceListener"/> class.
/// </summary>
/// <param name="fileName">The filename where the entries will be logged.</param>
/// <param name="name">Name of the trace listener.</param>
/// <param name="rollSizeKB">The maximum file size (KB) before rolling.</param>
public RollingFileTraceListener(
string fileName,
string name,
int rollSizeKB)
: base(OpenTextWriter(fileName), name)
{
if (fileName.IsNullOrWhiteSpace())
{
throw new ArgumentException("fileName was null or whitespace", nameof(fileName));
}
if (rollSizeKB <= 0)
{
throw new ArgumentOutOfRangeException(nameof(rollSizeKB));
}
TraceFileName = fileName;
_rollSizeInBytes = rollSizeKB * 1024;
RollingHelper = new StreamWriterRollingHelper(this);
}
/// <summary>
/// Gets name of the Trace file.
/// </summary>
internal string TraceFileName
{
get;
private set;
}
/// <summary>
/// Gets the <see cref="StreamWriterRollingHelper"/> for the flat file.
/// </summary>
/// <value>
/// The <see cref="StreamWriterRollingHelper"/> for the flat file.
/// </value>
internal StreamWriterRollingHelper RollingHelper { get; }
/// <summary>
/// Writes the trace messages to the file.
/// </summary>
/// <param name="message">Trace message string</param>
public override void WriteLine(string? message)
{
RollingHelper.RollIfNecessary();
base.WriteLine(message);
}
/// <summary>
/// Opens specified file and returns text writer.
/// </summary>
/// <param name="fileName">The file to open.</param>
/// <returns>A <see cref="TallyKeepingFileStreamWriter"/> instance.</returns>
internal static TallyKeepingFileStreamWriter OpenTextWriter(string fileName)
{
return new TallyKeepingFileStreamWriter(
File.Open(fileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite),
GetEncodingWithFallback());
}
/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
if (_isDisposed)
return;
if (disposing)
{
RollingHelper.Dispose();
}
base.Dispose(disposing);
_isDisposed = true;
}
private static Encoding GetEncodingWithFallback()
{
Encoding encoding = (Encoding)new UTF8Encoding(false).Clone();
#if TODO
encoding.EncoderFallback = EncoderFallback.ReplacementFallback;
encoding.DecoderFallback = DecoderFallback.ReplacementFallback;
#endif
return encoding;
}
/// <summary>
/// Encapsulates the logic to perform rolls.
/// </summary>
/// <remarks>
/// If no rolling behavior has been configured no further processing will be performed.
/// </remarks>
internal sealed class StreamWriterRollingHelper : IDisposable
{
/// <summary>
/// Synchronization lock.
/// </summary>
private readonly object _synclock = new();
/// <summary>
/// Whether the object is disposed or not.
/// </summary>
private bool _disposed;
/// <summary>
/// A tally keeping writer used when file size rolling is configured.<para/>
/// The original stream writer from the base trace listener will be replaced with
/// this listener.
/// </summary>
private TallyKeepingFileStreamWriter? _managedWriter;
private bool _lastRollFailed;
private readonly Stopwatch _sinceLastRoll = Stopwatch.StartNew();
private readonly long _failedRollTimeout = TimeSpan.FromMinutes(1).Ticks;
/// <summary>
/// The trace listener for which rolling is being managed.
/// </summary>
private readonly RollingFileTraceListener _owner;
/// <summary>
/// Initializes a new instance of the <see cref="StreamWriterRollingHelper"/> class.
/// </summary>
/// <param name="owner">
/// The <see cref="RollingFileTraceListener"/> to use.
/// </param>
public StreamWriterRollingHelper(RollingFileTraceListener owner)
{
_owner = owner;
_managedWriter = owner.Writer as TallyKeepingFileStreamWriter;
}
/// <summary>
/// Checks whether rolling should be performed, and returns the date to use when performing the roll.
/// </summary>
/// <returns>The date roll to use if performing a roll, or <see langword="null"/> if no rolling should occur.</returns>
/// <remarks>
/// Defer request for the roll date until it is necessary to avoid overhead.<para/>
/// Information used for rolling checks should be set by now.
/// </remarks>
public DateTime? CheckIsRollNecessary()
{
// check for size roll, if enabled.
if (_managedWriter != null && _managedWriter.Tally > _owner._rollSizeInBytes)
{
return DateTime.Now;
}
// no roll is necessary, return a null roll date
return null;
}
/// <summary>
/// Perform the roll for the next date.
/// </summary>
/// <param name="rollDateTime">The roll date.</param>
public void PerformRoll(DateTime rollDateTime)
{
string actualFileName = ((FileStream)((StreamWriter)_owner.Writer!).BaseStream).Name;
// calculate archive name
string directory = Path.GetDirectoryName(actualFileName)!;
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(actualFileName);
string extension = Path.GetExtension(actualFileName);
StringBuilder fileNameBuilder = new(fileNameWithoutExtension);
fileNameBuilder.Append('.');
fileNameBuilder.Append("bak");
fileNameBuilder.Append(extension);
string archiveFileName = Path.Combine(directory, fileNameBuilder.ToString());
#if TODO
// close file
owner.Writer.Close();
#endif
// move file
SafeMove(actualFileName, archiveFileName, rollDateTime);
// update writer - let TWTL open the file as needed to keep consistency
_owner.Writer = null;
_managedWriter = null;
UpdateRollingInformationIfNecessary();
}
/// <summary>
/// Rolls the file if necessary.
/// </summary>
public void RollIfNecessary()
{
if (!UpdateRollingInformationIfNecessary())
{
// an error was detected while handling roll information - avoid further processing
return;
}
DateTime? rollDateTime;
if ((rollDateTime = CheckIsRollNecessary()) != null)
{
lock (_synclock)
{
// double check if the roll is still required and do it...
if ((rollDateTime = CheckIsRollNecessary()) != null)
{
PerformRoll(rollDateTime.Value);
}
}
}
}
/// <summary>
/// Updates book keeping information necessary for rolling, as required by the specified
/// rolling configuration.
/// </summary>
/// <returns>true if update was successful, false if an error occurred.</returns>
public bool UpdateRollingInformationIfNecessary()
{
// if we fail to move the file, don't retry to move it for a long time
// it is most likely locked, and we end up trying to move it twice
// on each message write.
if (_lastRollFailed && _sinceLastRoll.ElapsedTicks < _failedRollTimeout)
{
return false;
}
// replace writer with the tally keeping version if necessary for size rolling
if (_managedWriter == null)
{
try
{
_managedWriter = OpenTextWriter(_owner.TraceFileName);
}
catch (Exception)
{
// there's a slight chance of error here - abort if this occurs and just let TWTL handle it without attempting to roll
return false;
}
_owner.Writer = _managedWriter;
}
return true;
}
/// <summary>
/// Disposes this instance
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void SafeMove(string actualFileName, string archiveFileName, DateTime currentDateTime)
{
try
{
if (File.Exists(archiveFileName))
{
File.Delete(archiveFileName);
}
// take care of tunneling issues http://support.microsoft.com/kb/172190
File.SetCreationTime(actualFileName, currentDateTime);
File.Move(actualFileName, archiveFileName);
_lastRollFailed = false;
_sinceLastRoll.Restart();
}
catch (IOException)
{
_lastRollFailed = true;
// catch errors and attempt move to a new file with a GUID
archiveFileName += Guid.NewGuid().ToString();
try
{
File.Move(actualFileName, archiveFileName);
_lastRollFailed = false;
_sinceLastRoll.Restart();
}
catch (IOException)
{
_lastRollFailed = true;
}
}
}
private void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing && _managedWriter != null)
{
#if TODO
managedWriter.Close();
#endif
}
_disposed = true;
}
}
/// <summary>
/// Represents a file stream writer that keeps a tally of the length of the file.
/// </summary>
internal sealed class TallyKeepingFileStreamWriter : StreamWriter
{
/// <summary>
/// Initializes a new instance of the <see cref="TallyKeepingFileStreamWriter"/> class.
/// </summary>
/// <param name="stream">
/// The <see cref="FileStream"/> to write to.
/// </param>
public TallyKeepingFileStreamWriter(FileStream stream)
: base(stream)
{
Tally = stream.Length;
}
/// <summary>
/// Initializes a new instance of the <see cref="TallyKeepingFileStreamWriter"/> class.
/// </summary>
/// <param name="stream">
/// The <see cref="FileStream"/> to write to.
/// </param>
/// <param name="encoding">
/// The <see cref="Encoding"/> to use.
/// </param>
public TallyKeepingFileStreamWriter(FileStream stream, Encoding encoding)
: base(stream, encoding)
{
Tally = stream.Length;
}
/// <summary>
/// Gets the tally of the length of the string.
/// </summary>
/// <value>
/// The tally of the length of the string.
/// </value>
public long Tally { get; private set; }
/// <summary>
/// Writes a character to the stream.
/// </summary>
/// <param name="value">
/// The character to write to the text stream.
/// </param>
/// <exception cref="T:System.ObjectDisposedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see>is true or the<see cref="T:System.IO.StreamWriter"></see>buffer is full, and current writer is closed.
/// </exception>
/// <exception cref="T:System.NotSupportedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see>is true or the<see cref="T:System.IO.StreamWriter"></see>buffer is full, and the contents of the buffer cannot be written to the underlying fixed size stream because the<see cref="T:System.IO.StreamWriter"></see>is at the end the stream.
/// </exception>
/// <exception cref="T:System.IO.IOException">
/// An I/O error occurs.
/// </exception>
/// <filterpriority>1</filterpriority>
public override void Write(char value)
{
base.Write(value);
Tally += Encoding.GetByteCount(new[] { value });
}
/// <summary>
/// Writes a character array to the stream.
/// </summary>
/// <param name="buffer">
/// A character array containing the data to write. If buffer is null, nothing is written.
/// </param>
/// <exception cref="T:System.ObjectDisposedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see>is true or the<see cref="T:System.IO.StreamWriter"></see>buffer is full, and current writer is closed.
/// </exception>
/// <exception cref="T:System.NotSupportedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see>is true or the<see cref="T:System.IO.StreamWriter"></see>buffer is full, and the contents of the buffer cannot be written to the underlying fixed size stream because the<see cref="T:System.IO.StreamWriter"></see>is at the end the stream.
/// </exception>
/// <exception cref="T:System.IO.IOException">
/// An I/O error occurs.
/// </exception>
/// <filterpriority>1</filterpriority>
public override void Write(char[]? buffer)
{
base.Write(buffer);
Tally += (buffer is not null) ? Encoding.GetByteCount(buffer) : 0;
}
/// <summary>
/// Writes an array of characters to the stream.
/// </summary>
/// <param name="buffer">
/// A character array containing the data to write.
/// </param>
/// <param name="index">
/// The index into buffer at which to begin writing.
/// </param>
/// <param name="count">
/// The number of characters to read from buffer.
/// </param>
/// <exception cref="T:System.IO.IOException">
/// An I/O error occurs.
/// </exception>
/// <exception cref="T:System.ObjectDisposedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see>is true or the<see cref="T:System.IO.StreamWriter"></see>buffer is full, and current writer is closed.
/// </exception>
/// <exception cref="T:System.NotSupportedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see>is true or the<see cref="T:System.IO.StreamWriter"></see>buffer is full, and the contents of the buffer cannot be written to the underlying fixed size stream because the<see cref="T:System.IO.StreamWriter"></see>is at the end the stream.
/// </exception>
/// <exception cref="T:System.ArgumentOutOfRangeException">
/// index or count is negative.
/// </exception>
/// <exception cref="T:System.ArgumentException">
/// The buffer length minus index is less than count.
/// </exception>
/// <exception cref="T:System.ArgumentNullException">
/// buffer is null.
/// </exception>
/// <filterpriority>1</filterpriority>
public override void Write(char[] buffer, int index, int count)
{
base.Write(buffer, index, count);
Tally += Encoding.GetByteCount(buffer, index, count);
}
/// <summary>
/// Writes a string to the stream.
/// </summary>
/// <param name="value">
/// The string to write to the stream. If value is null, nothing is written.
/// </param>
/// <exception cref="T:System.ObjectDisposedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see>is true or the<see cref="T:System.IO.StreamWriter"></see>buffer is full, and current writer is closed.
/// </exception>
/// <exception cref="T:System.NotSupportedException">
/// <see cref="P:System.IO.StreamWriter.AutoFlush"></see>is true or the<see cref="T:System.IO.StreamWriter"></see>buffer is full, and the contents of the buffer cannot be written to the underlying fixed size stream because the<see cref="T:System.IO.StreamWriter"></see>is at the end the stream.
/// </exception>
/// <exception cref="T:System.IO.IOException">
/// An I/O error occurs.
/// </exception>
/// <filterpriority>1</filterpriority>
public override void Write(string? value)
{
base.Write(value);
Tally += (value is not null) ? Encoding.GetByteCount(value) : 0;
}
}
}
|