niedziela, 23 stycznia 2011

SpecialFolders i zmienne systemowe w definicji TraceListener'a [PL]

Wpisy na temat śledzenia i logowania w .NET pojawiały się już wcześniej na tym blogu (np. „Śledzenie i logowanie zdarzeń (tracing and logging) na platformie .NET (przykłady w oparciu o C#).” i „Śledzimy w .NET dalej (dzisiaj uruchomimy własny podsłuch)”). Mechanizm wykorzystania elementów platformy .NET, jak: TraceSource, TraceListener i innych związanych z nimi jest dość wygodny, tym bardziej, że są dostępne gotowe klasy typu TraceListener zapisujące w plikach logi przy pomocy XML-a lub w których każdy element jest oddzielony jakimś znakiem rozdzielającym (np. przecinkiem, czy średnikiem). Te klasy to XmlWriterTraceListener lub DelimitedListTraceListener. Klasy te można łatwo utworzyć w pliku app.config przekazując nazwę pliku, do którego zostanie zapisany log. Niestety w nazwie pliku trzeba posłużyć się pełną ścieżką, by móc w pełni kontrolować jego lokalizację, w przeciwnym razie plik zostanie utworzony w kartotece, z której uruchamiana jest aplikacja. Zobaczmy jak można spowodować, by w nazwie wykorzystywać dodatkowo „foldery specjalne” i zmienne systemowe.
Otóż klasy XmlWriterTraceListener lub DelimitedListTraceListener, dziedziczą po wspólnej klasie TextWriterTraceListener, która zawiera definicję i obsługę strumienia (TextWriter), do którego realizowany jest zapis. Strumień ten jest dostępny we właściwości Writer. Do opracowania funkcjonalności, w której do stworzenia nazwy pliku wykorzystamy „foldery specjalne” i zmienne systemowe należy więc spowodować zmianę lokalizacji strumienia. Ponieważ poszczególne TraceListener'y są najczęściej deklarowane w pliku konfiguracyjnym (app.config), więc zmianę lokalizacji strumienia musimy wykonać już na etapie konstruktora. Dlatego najlepszym rozwiązaniem wydaje się odziedziczenie (po XmlWriterTraceListener lub DelimitedListTraceListener) i stworzenie własnych klas, które w konstruktorze zmienią lokalizację strumienia. Najprostszym rozwiązaniem mogłoby być wykonanie w konstruktorze podstawienia typu: this.Writer = new StreamWriter( newFileName );, ale powstaje pytanie co z poprzednim strumieniem? Czy może nie trzeba go zamknąć? Wydaje mi się, że lepiej sprawdzić przy pomocy IlDasm lub Reflector, jak jest zbudowana klasa TextWriterTraceListener. Otóż okazuje się, że w konstruktorze TextWriterTraceListener(string) wcale nie jest tworzony strumień, a jedynie nazwa postawiana jest do prywatnego pola fileName. Strumień tworzony jest dopiero później przy pomocy niepublicznej funkcji EnsureWriter(), która wywoływana jest przed każdym dostępie do strumienia. Otóż okazuje się, że EnsureWriter robi znacznie więcej, niż tylko tworzy strumień. Funkcja ta jest odpowiedzialna za obsługę błędów, które mogą się pojawić podczas tworzenia strumienia, potrafi nawet zmienić tak nazwę pliku (np. na Guid), by utworzenie takiego pliku było możliwe. Dlatego wydaje się, że lepszym rozwiązaniem jest podstawienie nowej nazwy właśnie do tego prywatnego pola fileName, a zrobić to można przy pomocy Reflection.
Ostatecznie kod dostarczających nowy TraceListener'ów może wyglądać następująco:
using System;
using System.Collections;
using System.Diagnostics;
using System.Reflection;
using System.Text.RegularExpressions;

namespace AdvancedListener
{
  internal class AdvancedTraceListenerHelper
  {
    private class AdvancedTraceListenerException: Exception
    {
      internal AdvancedTraceListenerException( string message ) : base( message ) { }
    }
    const string specialFormatToken = "|{0}|";
    const string environmentVariablePattern = "\\%{0}\\%";
    private static string PrepareFileName( string fileName )
    {
      foreach ( Environment.SpecialFolder folder in Enum.GetValues( typeof( Environment.SpecialFolder ) ) )
      {
        if ( !fileName.Contains( "|" ) )
          break;
        fileName = fileName.Replace( string.Format( specialFormatToken, folder.ToString() ), Environment.GetFolderPath( folder ) );
      }
      foreach ( DictionaryEntry variable in Environment.GetEnvironmentVariables() )
      {
        if ( !fileName.Contains( "%" ) )
          break;
        fileName = Regex.Replace( fileName, string.Format( environmentVariablePattern, (string)variable.Key ), (string)variable.Value, RegexOptions.IgnoreCase );
      }
      return fileName;
    }
    internal static void PrepareAndUpdateFilename( TextWriterTraceListener textWriterTraceListener, string fileName )
    {
      try
      {
        string org_filename = fileName;
        fileName = AdvancedTraceListenerHelper.PrepareFileName( fileName );
        if ( org_filename != fileName )
        {
          FieldInfo fi = typeof( TextWriterTraceListener ).GetField( "fileName", BindingFlags.NonPublic | BindingFlags.Instance );
          if ( fi != null )
            fi.SetValue( textWriterTraceListener, fileName );
          else
            throw new AdvancedTraceListenerException( "Current .NET framework is not supported." );
        }
      }
      catch ( AdvancedTraceListenerException ex )
      {
        throw ex;
      }
      catch ( Exception ) { }
    }
  }
  public class AdvancedDelimitedListTraceListener: DelimitedListTraceListener
  {
    public AdvancedDelimitedListTraceListener( string fileName )
      : base( fileName )
    {
      AdvancedTraceListenerHelper.PrepareAndUpdateFilename( this, fileName );
    }
  }
  public class AdvancedXmlWriterTraceListener: XmlWriterTraceListener
  {
    public AdvancedXmlWriterTraceListener( string fileName )
      : base( fileName )
    {
      AdvancedTraceListenerHelper.PrepareAndUpdateFilename( this, fileName );
    }
  }
}
Powyższy kod podstawia wartości zmiennych środowiskowych, gdy ich nazwę umieścimy między znakami % (podobnie jak w skryptach konsolowych), oraz lokalizację folderów specjalnych, gdy ich nazwę umieścimy między znakami | (podobnie jak w przypadku ConnectionString'ów w aplikacjach ASP.NET).
Jeżeli powyższy kod umieścimy w assembly o nazwie „AdvancedListener”, to może zostać wykorzystany następująco:
   <sharedListeners>
      <add name="LogFileDelimited" type="AdvancedListener.AdvancedDelimitedListTraceListener,AdvancedListener" initializeData ="|CommonApplicationData|\Application_Main2.log"
           traceOutputOptions="DateTime">
        <filter type="System.Diagnostics.EventTypeFilter" initializeData="All" />
      </add>
      <add name="LogFileXml" type="AdvancedListener.AdvancedXmlWriterTraceListener,AdvancedListener" initializeData ="%ALLUSERSPROFILE%\Application_Main3.log"
           traceOutputOptions="DateTime">
        <filter type="System.Diagnostics.EventTypeFilter" initializeData="All" />
      </add>
    </sharedListeners>
Proste prawda?
Promuj

Brak komentarzy:

Prześlij komentarz

Posty powiązane / Related posts