blob: 22c358813c93efdb0f432e7edf4c7cf3632429fd [file] [log] [blame]
#region Apache License
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to you under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
#endregion
using System;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
using log4net.Util;
using log4net.Layout;
using log4net.Core;
using System.Threading.Tasks;
namespace log4net.Appender;
/// <summary>
/// Appends logging events to a file.
/// </summary>
/// <remarks>
/// <para>
/// Logging events are sent to the file specified by the <see cref="File"/> property.
/// </para>
/// <para>
/// The file can be opened in either append or overwrite mode
/// by specifying the <see cref="AppendToFile"/> property.
/// If the file path is relative it is taken as relative from
/// the application base directory. The file encoding can be
/// specified by setting the <see cref="Encoding"/> property.
/// </para>
/// <para>
/// The layout's <see cref="ILayout.Header"/> and <see cref="ILayout.Footer"/>
/// values will be written each time the file is opened and closed
/// respectively. If the <see cref="AppendToFile"/> property is <see langword="true"/>
/// then the file may contain multiple copies of the header and footer.
/// </para>
/// <para>
/// This appender will first try to open the file for writing when <see cref="ActivateOptions"/>
/// is called. This will typically be during configuration.
/// If the file cannot be opened for writing the appender will attempt
/// to open the file again each time a message is logged to the appender.
/// If the file cannot be opened for writing when a message is logged then
/// the message will be discarded by this appender.
/// </para>
/// <para>
/// The <see cref="FileAppender"/> supports pluggable file locking models via
/// the <see cref="LockingModel"/> property.
/// The default behavior, implemented by <see cref="ExclusiveLock"/>
/// is to obtain an exclusive write lock on the file until this appender is closed.
/// The alternative models only hold a
/// write lock while the appender is writing a logging event (<see cref="MinimalLock"/>)
/// or synchronize by using a named system-wide Mutex (<see cref="InterProcessLock"/>).
/// </para>
/// <para>
/// All locking strategies have issues and you should seriously consider using a different strategy that
/// avoids having multiple processes logging to the same file.
/// </para>
/// </remarks>
/// <author>Nicko Cadell</author>
/// <author>Gert Driesen</author>
/// <author>Rodrigo B. de Oliveira</author>
/// <author>Douglas de la Torre</author>
/// <author>Niall Daley</author>
public class FileAppender : TextWriterAppender
{
/// <summary>
/// Write only <see cref="Stream"/> that uses the <see cref="LockingModelBase"/>
/// to manage access to an underlying resource.
/// </summary>
private sealed class LockingStream(LockingModelBase lockingModel) : Stream, IDisposable
{
[Log4NetSerializable]
public sealed class LockStateException : LogException
{
public LockStateException(string message)
: base(message)
{ }
public LockStateException()
{ }
public LockStateException(string message, Exception innerException)
: base(message, innerException)
{ }
private LockStateException(SerializationInfo info, StreamingContext context)
: base(info, context)
{ }
}
private readonly object _syncRoot = new();
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "todo")]
private Stream? _realStream;
private int _lockLevel;
protected override void Dispose(bool disposing)
{
lockingModel.CloseFile();
base.Dispose(disposing);
}
public override Task<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
=> AssertLocked().ReadAsync(buffer, offset, count, cancellationToken);
public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
{
AssertLocked();
return base.WriteAsync(buffer, offset, count, cancellationToken);
}
public override void Flush() => AssertLocked().Flush();
public override int Read(byte[] buffer, int offset, int count)
=> AssertLocked().Read(buffer, offset, count);
public override int ReadByte() => AssertLocked().ReadByte();
public override long Seek(long offset, SeekOrigin origin)
=> AssertLocked().Seek(offset, origin);
public override void SetLength(long value) => AssertLocked().SetLength(value);
void IDisposable.Dispose() => Dispose(true);
public override void Write(byte[] buffer, int offset, int count)
=> AssertLocked().Write(buffer, offset, count);
public override void WriteByte(byte value) => AssertLocked().WriteByte(value);
// Properties
public override bool CanRead => false;
public override bool CanSeek => AssertLocked().CanSeek;
public override bool CanWrite => AssertLocked().CanWrite;
public override long Length => AssertLocked().Length;
public override long Position
{
get => AssertLocked().Position;
set => AssertLocked().Position = value;
}
private Stream AssertLocked()
{
if (_realStream is null)
{
throw new LockStateException("The file is not currently locked");
}
return _realStream;
}
public bool AcquireLock()
{
bool ret = false;
lock (_syncRoot)
{
if (_lockLevel == 0)
{
// If lock is already acquired, nop
_realStream = lockingModel.AcquireLock();
}
if (_realStream is not null)
{
_lockLevel++;
ret = true;
}
}
return ret;
}
public void ReleaseLock()
{
lock (_syncRoot)
{
_lockLevel--;
if (_lockLevel == 0)
{
// If already unlocked, nop
lockingModel.ReleaseLock();
_realStream = null;
}
}
}
}
/// <summary>
/// Locking model base class
/// </summary>
/// <remarks>
/// <para>
/// Base class for the locking models available to the <see cref="FileAppender"/> derived loggers.
/// </para>
/// </remarks>
public abstract class LockingModelBase
{
/// <summary>
/// Open the output file
/// </summary>
/// <param name="filename">The filename to use</param>
/// <param name="append">Whether to append to the file, or overwrite</param>
/// <param name="encoding">The encoding to use</param>
/// <remarks>
/// <para>
/// Open the file specified and prepare for logging.
/// No writes will be made until <see cref="AcquireLock"/> is called.
/// Must be called before any calls to <see cref="AcquireLock"/>,
/// <see cref="ReleaseLock"/> and <see cref="CloseFile"/>.
/// </para>
/// </remarks>
public abstract void OpenFile(string filename, bool append, Encoding encoding);
/// <summary>
/// Close the file
/// </summary>
/// <remarks>
/// <para>
/// Close the file. No further writes will be made.
/// </para>
/// </remarks>
public abstract void CloseFile();
/// <summary>
/// Initializes all resources used by this locking model.
/// </summary>
public abstract void ActivateOptions();
/// <summary>
/// Disposes all resources that were initialized by this locking model.
/// </summary>
public abstract void OnClose();
/// <summary>
/// Acquire the lock on the file
/// </summary>
/// <returns>A stream that is ready to be written to, or null if there is no active stream because uninitialized or error.</returns>
/// <remarks>
/// <para>
/// Acquire the lock on the file in preparation for writing to it.
/// Returns a stream pointing to the file. <see cref="ReleaseLock"/>
/// must be called to release the lock on the output file when the return
/// value is not null.
/// </para>
/// </remarks>
public abstract Stream? AcquireLock();
/// <summary>
/// Releases the lock on the file
/// </summary>
/// <remarks>
/// <para>
/// No further writes will be made to the stream until <see cref="AcquireLock"/> is called again.
/// </para>
/// </remarks>
public abstract void ReleaseLock();
/// <summary>
/// Gets or sets the <see cref="FileAppender"/> for this LockingModel
/// </summary>
/// <value>
/// The <see cref="FileAppender"/> for this LockingModel
/// </value>
/// <remarks>
/// <para>
/// The file appender this locking model is attached to and working on
/// behalf of.
/// </para>
/// <para>
/// The file appender is used to locate the security context and the error handler to use.
/// </para>
/// <para>
/// The value of this property will be set before <see cref="OpenFile"/> is
/// called.
/// </para>
/// </remarks>
public FileAppender? CurrentAppender { get; set; }
/// <summary>
/// Helper method that creates a FileStream under CurrentAppender's SecurityContext.
/// </summary>
/// <remarks>
/// <para>
/// Typically called during OpenFile or AcquireLock.
/// </para>
/// <para>
/// If the directory portion of the <paramref name="filename"/> does not exist, it is created
/// via Directory.CreateDirectory.
/// </para>
/// </remarks>
/// <param name="filename"></param>
/// <param name="append"></param>
/// <param name="fileShare"></param>
/// <returns></returns>
protected Stream CreateStream(string filename, bool append, FileShare fileShare)
{
filename = Environment.ExpandEnvironmentVariables(filename);
using (CurrentAppender?.SecurityContext?.Impersonate(this))
{
// Ensure that the directory structure exists
string? directoryFullName = Path.GetDirectoryName(filename);
// Only create the directory if it does not exist
// doing this check here resolves some permissions failures
if (directoryFullName is not null && !Directory.Exists(directoryFullName))
{
Directory.CreateDirectory(directoryFullName);
}
FileMode fileOpenMode = append
? FileMode.Append
: FileMode.Create;
return new FileStream(filename, fileOpenMode, FileAccess.Write, fileShare);
}
}
/// <summary>
/// Helper method to close <paramref name="stream"/> under CurrentAppender's SecurityContext.
/// </summary>
/// <remarks>
/// Does not set <paramref name="stream"/> to null.
/// </remarks>
/// <param name="stream"></param>
protected void CloseStream(Stream stream)
{
using var _ = CurrentAppender?.SecurityContext?.Impersonate(this);
stream?.Dispose();
}
}
/// <summary>
/// Hold an exclusive lock on the output file
/// </summary>
/// <remarks>
/// <para>
/// Open the file once for writing and hold it open until <see cref="CloseFile"/> is called.
/// Maintains an exclusive lock on the file during this time.
/// </para>
/// </remarks>
public class ExclusiveLock : LockingModelBase
{
private Stream? _stream;
/// <summary>
/// Open the file specified and prepare for logging.
/// </summary>
/// <param name="filename">The filename to use</param>
/// <param name="append">Whether to append to the file, or overwrite</param>
/// <param name="encoding">The encoding to use</param>
/// <remarks>
/// <para>
/// Open the file specified and prepare for logging.
/// No writes will be made until <see cref="AcquireLock"/> is called.
/// Must be called before any calls to <see cref="AcquireLock"/>,
/// <see cref="ReleaseLock"/> and <see cref="CloseFile"/>.
/// </para>
/// </remarks>
public override void OpenFile(string filename, bool append, Encoding encoding)
{
try
{
_stream = CreateStream(filename, append, FileShare.Read);
}
catch (Exception e) when (!e.IsFatal())
{
CurrentAppender?.ErrorHandler.Error($"Unable to acquire lock on file {filename}. {e.Message}");
}
}
/// <summary>
/// Close the file
/// </summary>
/// <remarks>
/// <para>
/// Close the file. No further writes will be made.
/// </para>
/// </remarks>
public override void CloseFile()
{
if (_stream is not null)
{
CloseStream(_stream);
_stream = null;
}
}
/// <summary>
/// Acquire the lock on the file
/// </summary>
/// <returns>A stream that is ready to be written to.</returns>
/// <remarks>
/// <para>
/// Does nothing. The lock is already taken
/// </para>
/// </remarks>
public override Stream? AcquireLock() => _stream;
/// <summary>
/// Release the lock on the file
/// </summary>
/// <remarks>
/// <para>
/// Does nothing. The lock will be released when the file is closed.
/// </para>
/// </remarks>
public override void ReleaseLock()
{
//NOP
}
/// <summary>
/// Initializes all resources used by this locking model.
/// </summary>
public override void ActivateOptions()
{
//NOP
}
/// <summary>
/// Disposes all resources that were initialized by this locking model.
/// </summary>
public override void OnClose()
{
//NOP
}
}
/// <summary>
/// Acquires the file lock for each write
/// </summary>
/// <remarks>
/// <para>
/// Opens the file once for each <see cref="AcquireLock"/>/<see cref="ReleaseLock"/> cycle,
/// thus holding the lock for the minimal amount of time. This method of locking
/// is considerably slower than <see cref="ExclusiveLock"/> but allows
/// other processes to move/delete the log file whilst logging continues.
/// </para>
/// </remarks>
public class MinimalLock : LockingModelBase
{
private string? _filename;
private bool _append;
private Stream? _stream;
/// <summary>
/// Prepares to open the file when the first message is logged.
/// </summary>
/// <param name="filename">The filename to use</param>
/// <param name="append">Whether to append to the file, or overwrite</param>
/// <param name="encoding">The encoding to use</param>
/// <remarks>
/// <para>
/// Open the file specified and prepare for logging.
/// No writes will be made until <see cref="AcquireLock"/> is called.
/// Must be called before any calls to <see cref="AcquireLock"/>,
/// <see cref="ReleaseLock"/> and <see cref="CloseFile"/>.
/// </para>
/// </remarks>
public override void OpenFile(string filename, bool append, Encoding encoding)
{
_filename = filename;
_append = append;
}
/// <summary>
/// Close the file
/// </summary>
/// <remarks>
/// <para>
/// Close the file. No further writes will be made.
/// </para>
/// </remarks>
public override void CloseFile()
{
// NOP
}
/// <summary>
/// Acquire the lock on the file
/// </summary>
/// <returns>A stream that is ready to be written to.</returns>
/// <remarks>
/// <para>
/// Acquire the lock on the file in preparation for writing to it.
/// Return a stream pointing to the file. <see cref="ReleaseLock"/>
/// must be called to release the lock on the output file.
/// </para>
/// </remarks>
public override Stream? AcquireLock()
{
if (_stream is null)
{
if (_filename is not null)
{
try
{
_stream = CreateStream(_filename, _append, FileShare.Read);
_append = true;
}
catch (Exception e) when (!e.IsFatal())
{
CurrentAppender?.ErrorHandler.Error($"Unable to acquire lock on file {_filename}. {e.Message}");
}
}
else
{
CurrentAppender?.ErrorHandler.Error($"Unable to acquire lock because {nameof(OpenFile)} has not been called");
}
}
return _stream;
}
/// <summary>
/// Release the lock on the file
/// </summary>
/// <remarks>
/// <para>
/// Release the lock on the file. No further writes will be made to the
/// stream until <see cref="AcquireLock"/> is called again.
/// </para>
/// </remarks>
public override void ReleaseLock()
{
if (_stream is not null)
{
CloseStream(_stream);
_stream = null;
}
}
/// <summary>
/// Initializes all resources used by this locking model.
/// </summary>
public override void ActivateOptions()
{
//NOP
}
/// <summary>
/// Disposes all resources that were initialized by this locking model.
/// </summary>
public override void OnClose()
{
//NOP
}
}
/// <summary>
/// Provides cross-process file locking.
/// </summary>
/// <author>Ron Grabowski</author>
/// <author>Steve Wranovsky</author>
public class InterProcessLock : LockingModelBase
{
private Mutex? _mutex;
private Stream? _stream;
private int _recursiveWatch;
/// <summary>
/// Open the file specified and prepare for logging.
/// </summary>
/// <param name="filename">The filename to use</param>
/// <param name="append">Whether to append to the file, or overwrite</param>
/// <param name="encoding">The encoding to use</param>
/// <remarks>
/// <para>
/// Open the file specified and prepare for logging.
/// No writes will be made until <see cref="AcquireLock"/> is called.
/// Must be called before any calls to <see cref="AcquireLock"/>,
/// -<see cref="ReleaseLock"/> and <see cref="CloseFile"/>.
/// </para>
/// </remarks>
[System.Security.SecuritySafeCritical]
public override void OpenFile(string filename, bool append, Encoding encoding)
{
try
{
_stream = CreateStream(filename, append, FileShare.ReadWrite);
}
catch (Exception e) when (!e.IsFatal())
{
CurrentAppender?.ErrorHandler.Error($"Unable to acquire lock on file {filename}. {e.Message}");
}
}
/// <summary>
/// Close the file
/// </summary>
/// <remarks>
/// <para>
/// Close the file. No further writes will be made.
/// </para>
/// </remarks>
public override void CloseFile()
{
try
{
if (_stream is not null)
{
CloseStream(_stream);
_stream = null;
}
}
finally
{
ReleaseLock();
}
}
/// <summary>
/// Acquire the lock on the file
/// </summary>
/// <returns>A stream that is ready to be written to.</returns>
/// <remarks>
/// <para>
/// Does nothing. The lock is already taken
/// </para>
/// </remarks>
public override Stream? AcquireLock()
{
if (_mutex is not null)
{
// TODO: add timeout?
_mutex.WaitOne();
// increment recursive watch
_recursiveWatch++;
// should always be true (and fast) for FileStream
if (_stream is not null)
{
if (_stream.CanSeek)
{
_stream.Seek(0, SeekOrigin.End);
}
}
else
{
// this can happen when the file appender cannot open a file for writing
}
}
else
{
CurrentAppender?.ErrorHandler.Error(
"Programming error, no mutex available to acquire lock! From here on things will be dangerous!");
}
return _stream;
}
/// <summary>
/// Releases the lock and allows others to acquire a lock.
/// </summary>
public override void ReleaseLock()
{
if (_mutex is not null)
{
if (_recursiveWatch > 0)
{
_recursiveWatch--;
_mutex.ReleaseMutex();
}
}
else
{
CurrentAppender?.ErrorHandler.Error("Programming error, no mutex available to release the lock!");
}
}
/// <summary>
/// Initializes all resources used by this locking model.
/// </summary>
public override void ActivateOptions()
{
if (_mutex is null)
{
if (CurrentAppender is not null)
{
if (CurrentAppender.File is not null)
{
string mutexFriendlyFilename = CurrentAppender.File
.Replace("\\", "_")
.Replace(":", "_")
.Replace("/", "_");
_mutex = new Mutex(false, mutexFriendlyFilename);
}
else
{
CurrentAppender.ErrorHandler.Error($"Current appender has no file name, {nameof(OpenFile)} not called or it encountered an error");
}
}
}
else
{
CurrentAppender?.ErrorHandler.Error("Programming error, mutex already initialized!");
}
}
/// <summary>
/// Disposes all resources that were initialized by this locking model.
/// </summary>
public override void OnClose()
{
if (_mutex is not null)
{
_mutex.Dispose();
_mutex = null;
}
else
{
CurrentAppender?.ErrorHandler.Error("Programming error, mutex not initialized!");
}
}
}
/// <summary>
/// Hold no lock on the output file
/// </summary>
/// <remarks>
/// <para>
/// Open the file once and hold it open until <see cref="CloseFile"/> is called.
/// Maintains no lock on the file during this time.
/// </para>
/// </remarks>
public class NoLock : LockingModelBase
{
private Stream? _stream;
/// <summary>
/// Open the file specified and prepare for logging.
/// </summary>
/// <param name="filename">The filename to use</param>
/// <param name="append">Whether to append to the file, or overwrite</param>
/// <param name="encoding">The encoding to use</param>
/// <remarks>
/// <para>
/// Open the file specified and prepare for logging.
/// No writes will be made until <see cref="AcquireLock"/> is called.
/// Must be called before any calls to <see cref="AcquireLock"/>,
/// <see cref="ReleaseLock"/> and <see cref="CloseFile"/>.
/// </para>
/// </remarks>
public override void OpenFile(string filename, bool append, Encoding encoding)
{
try
{
// no lock
_stream = CreateStream(filename, append, FileShare.ReadWrite);
}
catch (Exception e) when (!e.IsFatal())
{
CurrentAppender?.ErrorHandler.Error($"Unable to acquire lock on file {filename}. {e.Message}");
}
}
/// <summary>
/// Close the file
/// </summary>
/// <remarks>
/// <para>
/// Close the file. No further writes will be made.
/// </para>
/// </remarks>
public override void CloseFile()
{
if (_stream is not null)
{
CloseStream(_stream);
_stream = null;
}
}
/// <summary>
/// Acquire the lock on the file
/// </summary>
/// <returns>A stream that is ready to be written to.</returns>
/// <remarks>
/// <para>
/// Does nothing. The lock is already taken
/// </para>
/// </remarks>
public override Stream? AcquireLock() => _stream;
/// <summary>
/// Release the lock on the file
/// </summary>
/// <remarks>
/// <para>
/// Does nothing. The lock will be released when the file is closed.
/// </para>
/// </remarks>
public override void ReleaseLock()
{
// NOP
}
/// <summary>
/// Initializes all resources used by this locking model.
/// </summary>
public override void ActivateOptions()
{
// NOP
}
/// <summary>
/// Disposes all resources that were initialized by this locking model.
/// </summary>
public override void OnClose()
{
// NOP
}
}
/// <summary>
/// Default locking model (when no locking model was configured)
/// </summary>
private static Type _defaultLockingModelType = typeof(ExclusiveLock);
/// <summary>
/// Specify default locking model
/// </summary>
/// <typeparam name="TLockingModel">Type of LockingModel</typeparam>
public static void SetDefaultLockingModelType<TLockingModel>()
where TLockingModel : LockingModelBase
=> _defaultLockingModelType = typeof(TLockingModel);
/// <summary>
/// Gets or sets the path to the file that logging will be written to.
/// </summary>
/// <value>
/// The path to the file that logging will be written to.
/// </value>
/// <remarks>
/// <para>
/// If the path is relative it is taken as relative from
/// the application base directory.
/// </para>
/// </remarks>
public virtual string? File
{
get => _fileName;
set => _fileName = value;
}
/// <summary>
/// Gets or sets a flag that indicates whether the file should be
/// appended to or overwritten.
/// </summary>
/// <value>
/// Indicates whether the file should be appended to or overwritten.
/// </value>
/// <remarks>
/// <para>
/// If the value is set to false then the file will be overwritten, if
/// it is set to true then the file will be appended to.
/// </para>
/// The default value is true.
/// </remarks>
public bool AppendToFile { get; set; } = true;
/// <summary>
/// Gets or sets <see cref="Encoding"/> used to write to the file.
/// </summary>
/// <value>
/// The <see cref="Encoding"/> used to write to the file.
/// </value>
/// <remarks>
/// <para>
/// The default encoding set is <see cref="Encoding.Default"/>
/// which is the encoding for the system's current ANSI code page.
/// </para>
/// </remarks>
public Encoding Encoding { get; set; } = Encoding.GetEncoding(0);
/// <summary>
/// Gets or sets the <see cref="SecurityContext"/> used to write to the file.
/// </summary>
/// <value>
/// The <see cref="SecurityContext"/> used to write to the file.
/// </value>
/// <remarks>
/// <para>
/// Unless a <see cref="SecurityContext"/> specified here for this appender
/// the <see cref="SecurityContextProvider.DefaultProvider"/> is queried for the
/// security context to use. The default behavior is to use the security context
/// of the current thread.
/// </para>
/// </remarks>
public SecurityContext? SecurityContext { get; set; }
/// <summary>
/// Gets or sets the <see cref="LockingModel"/> used to handle locking of the file.
/// </summary>
/// <value>
/// The <see cref="LockingModel"/> used to lock the file.
/// </value>
/// <remarks>
/// <para>
/// Gets or sets the <see cref="LockingModel"/> used to handle locking of the file.
/// </para>
/// <para>
/// There are three built in locking models, <see cref="ExclusiveLock"/>, <see cref="MinimalLock"/> and <see cref="InterProcessLock"/> .
/// The first locks the file from the start of logging to the end, the
/// second locks only for the minimal amount of time when logging each message
/// and the last synchronizes processes using a named system-wide Mutex.
/// </para>
/// <para>
/// The default locking model is the <see cref="ExclusiveLock"/>.
/// </para>
/// </remarks>
public LockingModelBase LockingModel { get; set; }
= Activator.CreateInstance(_defaultLockingModelType).EnsureIs<LockingModelBase>();
/// <summary>
/// Activate the options on the file appender.
/// </summary>
/// <remarks>
/// <para>
/// This is part of the <see cref="IOptionHandler"/> delayed object
/// activation scheme. The <see cref="ActivateOptions"/> method must
/// be called on this object after the configuration properties have
/// been set. Until <see cref="ActivateOptions"/> is called this
/// object is in an undefined state and must not be used.
/// </para>
/// <para>
/// If any of the configuration properties are modified then
/// <see cref="ActivateOptions"/> must be called again.
/// </para>
/// <para>
/// This will cause the file to be opened.
/// </para>
/// </remarks>
public override void ActivateOptions()
{
base.ActivateOptions();
SecurityContext ??= SecurityContextProvider.DefaultProvider.CreateSecurityContext(this);
LockingModel.CurrentAppender = this;
LockingModel.ActivateOptions();
if (_fileName is not null)
{
using (SecurityContext.Impersonate(this))
{
_fileName = ConvertToFullPath(_fileName.Trim());
}
SafeOpenFile(_fileName, AppendToFile);
}
else
{
LogLog.Warn(_declaringType, "FileAppender: File option not set for appender [" + Name + "].");
LogLog.Warn(_declaringType, "FileAppender: Are you using FileAppender instead of ConsoleAppender?");
}
}
/// <summary>
/// Closes any previously opened file and calls the parent's <see cref="TextWriterAppender.Reset"/>.
/// </summary>
/// <remarks>
/// <para>
/// Resets the filename and the file stream.
/// </para>
/// </remarks>
protected override void Reset()
{
base.Reset();
_fileName = null;
}
/// <summary>
/// Close this appender instance. The underlying stream or writer is also closed.
/// </summary>
protected override void OnClose()
{
base.OnClose();
LockingModel.OnClose();
}
/// <summary>
/// Called to initialize the file writer
/// </summary>
/// <remarks>
/// <para>
/// Will be called for each logged message until the file is
/// successfully opened.
/// </para>
/// </remarks>
protected override void PrepareWriter()
{
if (_fileName is not null)
{
SafeOpenFile(_fileName, AppendToFile);
}
}
/// <summary>
/// This method is called by the <see cref="AppenderSkeleton.DoAppend(LoggingEvent)"/>
/// method.
/// </summary>
/// <param name="loggingEvent">The event to log.</param>
/// <remarks>
/// <para>
/// Writes a log statement to the output stream if the output stream exists
/// and is writable.
/// </para>
/// <para>
/// The format of the output will depend on the appender's layout.
/// </para>
/// </remarks>
protected override void Append(LoggingEvent loggingEvent)
{
if (_stream is not null && _stream.AcquireLock())
{
try
{
base.Append(loggingEvent);
}
finally
{
_stream.ReleaseLock();
}
}
}
/// <summary>
/// This method is called by the <see cref="AppenderSkeleton.DoAppend(LoggingEvent[])"/>
/// method.
/// </summary>
/// <param name="loggingEvents">The array of events to log.</param>
/// <remarks>
/// <para>
/// Acquires the output file locks once before writing all the events to
/// the stream.
/// </para>
/// </remarks>
protected override void Append(LoggingEvent[] loggingEvents)
{
if (_stream is not null && _stream.AcquireLock())
{
try
{
base.Append(loggingEvents);
}
finally
{
_stream.ReleaseLock();
}
}
}
/// <summary>
/// Writes a footer as produced by the embedded layout's <see cref="ILayout.Footer"/> property.
/// </summary>
/// <remarks>
/// <para>
/// Writes a footer as produced by the embedded layout's <see cref="ILayout.Footer"/> property.
/// </para>
/// </remarks>
protected override void WriteFooter()
{
if (_stream is not null)
{
//WriteFooter can be called even before a file is opened
_stream.AcquireLock();
try
{
base.WriteFooter();
}
finally
{
_stream.ReleaseLock();
}
}
}
/// <summary>
/// Writes a header produced by the embedded layout's <see cref="ILayout.Header"/> property.
/// </summary>
/// <remarks>
/// <para>
/// Writes a header produced by the embedded layout's <see cref="ILayout.Header"/> property.
/// </para>
/// </remarks>
protected override void WriteHeader()
{
if (_stream is not null)
{
if (_stream.AcquireLock())
{
try
{
base.WriteHeader();
}
finally
{
_stream.ReleaseLock();
}
}
}
}
/// <summary>
/// Closes the underlying <see cref="TextWriter"/>.
/// </summary>
/// <remarks>
/// <para>
/// Closes the underlying <see cref="TextWriter"/>.
/// </para>
/// </remarks>
protected override void CloseWriter()
{
if (_stream is not null)
{
_stream.AcquireLock();
try
{
base.CloseWriter();
}
finally
{
_stream.ReleaseLock();
}
}
}
/// <summary>
/// Closes the previously opened file.
/// </summary>
/// <remarks>
/// <para>
/// Writes the <see cref="ILayout.Footer"/> to the file and then
/// closes the file.
/// </para>
/// </remarks>
protected void CloseFile() => WriteFooterAndCloseWriter();
/// <summary>
/// Sets and <i>opens</i> the file where the log output will go. The specified file must be writable.
/// </summary>
/// <param name="fileName">The path to the log file. Must be a fully qualified path.</param>
/// <param name="append">If true will append to fileName. Otherwise will truncate fileName</param>
/// <remarks>
/// <para>
/// Calls <see cref="OpenFile"/> but guarantees not to throw an exception.
/// Errors are passed to the <see cref="TextWriterAppender.ErrorHandler"/>.
/// </para>
/// </remarks>
protected virtual void SafeOpenFile(string fileName, bool append)
{
try
{
OpenFile(fileName, append);
}
catch (Exception e) when (!e.IsFatal())
{
ErrorHandler.Error($"OpenFile({fileName},{append}) call failed.", e, ErrorCode.FileOpenFailure);
}
}
/// <summary>
/// Sets and <i>opens</i> the file where the log output will go. The specified file must be writable.
/// </summary>
/// <param name="fileName">The path to the log file. Must be a fully qualified path.</param>
/// <param name="append">If true will append to fileName. Otherwise will truncate fileName</param>
/// <remarks>
/// <para>
/// If there was already an opened file, then the previous file
/// is closed first.
/// </para>
/// <para>
/// This method will ensure that the directory structure
/// for the <paramref name="fileName"/> specified exists.
/// </para>
/// </remarks>
protected virtual void OpenFile(string fileName, bool append)
{
if (LogLog.IsErrorEnabled)
{
// Internal check that the fileName passed in is a rooted path
bool isPathRooted;
using (SecurityContext?.Impersonate(this))
{
isPathRooted = Path.IsPathRooted(fileName);
}
if (!isPathRooted)
{
LogLog.Error(_declaringType,
"INTERNAL ERROR. OpenFile(" + fileName + "): File name is not fully qualified.");
}
}
lock (LockObj)
{
Reset();
LogLog.Debug(_declaringType, "Opening file for writing [" + fileName + "] append [" + append + "]");
// Save these for later, allowing retries if file open fails
_fileName = fileName;
AppendToFile = append;
LockingModel.CurrentAppender = this;
LockingModel.OpenFile(fileName, append, Encoding);
_stream = new LockingStream(LockingModel);
if (_stream is not null)
{
_stream.AcquireLock();
try
{
SetQWForFiles(_stream);
}
finally
{
_stream.ReleaseLock();
}
}
WriteHeader();
}
}
/// <summary>
/// Sets the quiet writer used for file output
/// </summary>
/// <param name="fileStream">the file stream that has been opened for writing</param>
/// <remarks>
/// <para>
/// This implementation of <see cref="SetQWForFiles(Stream)"/> creates a <see cref="StreamWriter"/>
/// over the <paramref name="fileStream"/> and passes it to the
/// <see cref="SetQWForFiles(TextWriter)"/> method.
/// </para>
/// <para>
/// This method can be overridden by subclasses that want to wrap the
/// <see cref="Stream"/> in some way, for example to encrypt the output
/// data using a <c>System.Security.Cryptography.CryptoStream</c>.
/// </para>
/// </remarks>
protected virtual void SetQWForFiles(Stream fileStream)
{
#pragma warning disable CA2000 // Dispose objects before losing scope
StreamWriter writer = new(fileStream, Encoding);
#pragma warning restore CA2000 // Dispose objects before losing scope
SetQWForFiles(writer);
}
/// <summary>
/// Sets the quiet writer being used.
/// </summary>
/// <param name="writer">the writer over the file stream that has been opened for writing</param>
/// <remarks>
/// <para>
/// This method can be overridden by subclasses that want to
/// wrap the <see cref="TextWriter"/> in some way.
/// </para>
/// </remarks>
protected virtual void SetQWForFiles(TextWriter writer)
=> QuietWriter = new(writer, ErrorHandler);
/// <summary>
/// Convert a path into a fully qualified path.
/// </summary>
/// <param name="path">The path to convert.</param>
/// <returns>The fully qualified path.</returns>
/// <remarks>
/// <para>
/// Converts the path specified to a fully
/// qualified path. If the path is relative it is
/// taken as relative from the application base
/// directory.
/// </para>
/// </remarks>
protected static string ConvertToFullPath(string path) => SystemInfo.ConvertToFullPath(path);
/// <summary>
/// The name of the log file.
/// </summary>
private string? _fileName;
/// <summary>
/// The stream to log to. Has added locking semantics
/// </summary>
private LockingStream? _stream;
/// <summary>
/// The fully qualified type of the FileAppender class.
/// </summary>
/// <remarks>
/// Used by the internal logger to record the Type of the
/// log message.
/// </remarks>
private static readonly Type _declaringType = typeof(FileAppender);
}