Get-Tail

Zwar gibt es schon sehr viele Programme, die das Ende einer Datei anzeigen (Siehe auch Tail) und selbst die Powershell hat mit dem Programm "Get-Content" und der Option "-WAIT" ein Hilfsmittel, um Dateien zeilenweise einzulesen und auf neu hinzugefügte Daten zu reagieren. Aber diese Tools sind doch entweder für den Admin oder interaktiven Gebrauch und weniger für eine automatische Verarbeitung neuer Daten. "GET-CONTENT -WAIT" z.B. liest trotzdem immer erst einmal die ganze Datei, ehe es auf mehr Input wartet.

Eine einfache Umsetzung in Form einer VBScript-Klasse zum fortgesetzten Lesen am Ende einer Datei habe ich auf VBSToolbox unter der URL class.tail.1.0.vbs veröffentlicht, Aber das kommt natürlich überhaupt nicht an das eigentliche Ziel heran

Anforderungen

Daher überlege ich schon lange, ob ich mit eine eigene TAIL-Klasse bzw. TAIL-Objekt erstelle, welche für mich essentielle Funktionen mitbringt. Es reicht mir nicht aus, einfach nur ein Programm mit einem Dateinamen aufzurufen, welches dann die neu angefügten Daten ausgibt.

Ich erwarte mir deutlich mehr als eine einfach "tail.exe". Ideal fände ich eine .NET DLL, welche als PSSnapin einfach nachgeladen werden kann. Vielleicht kann Sie auch noch per COM erreicht werden, damit die VB-Skript-Jünger auch noch etwas davon haben.

Einsatzbereiche

Nun werden Sie sich fragen, warum so was "einfaches" überhaupt entwickelt werden soll. Dazu möchte ich ein paar Beispiele aufführen. Weitere Einsatzbereiche finden Sie auch auf Tail.

Dateien sind eine einfache stabile und flexible Schnittstelle zur Übergabe von Informationen von einem Prozess zu einem anderen Dienst und funktionieren auch Betriebssystemübergreifend und über die meisten (Datei)-Netzwerke.

Wie verfolge ich Änderungen ?

Ich habe mir natürlich schon meine Gedanken gemacht. Es gibt gar mehrere Optionen, möglichst schnell zu erfahren, ob eine Datei sich geändert hat. Vier Verfahren konnte ich ausfindig machen:

$watcher = [system.io.filesystemwatcher]
$watcher.path = ""
watcher.Filter = "*.txt"
$watcher.NotifyFilter = (NotifyFilters.LastAccess Or NotifyFilters.LastWrite Or NotifyFilters.FileName Or NotifyFilters.DirectoryName)

Letztlich mache ich keine Vorgaben, wie die Lösung umzusetzen ist, Aus Kompatibilitätsgründen und der Frage der erforderlichen Berechtigungen könnte ich mir aber vorstellen, dass das fortgesetzte Lesen am Ende der Dateien und die Überwachung nach neuen bzw. gelöschten Dateien mit einem DIR eine sinnvolle Kombination sein könnte.

Wie lese ich Dateien ?

Wenn wir uns mal in der .NET-Welt bewegen, dann gibt es natürlich die vorhandenen .NET-Klassen:

Klasse Beschreibung
StreamReader TextReader

Diese Klasse ist ideal zum sequentiellen Lesen von Textdateien. Vor allem weil sie ein "ReadLine" unterstützt, welches einfach die nächste Zeile liest. Dummerweise unterstützt sie aber keine "Seek"-Funktion um bestimmte Stellen direkt anzuspringen. Man kann immer nur "vorwärts" lesen. Zurückspringen geht nicht. Man kann aber die Datei einfach schließen und wieder neu von vorne öffnen. Selbst die Funktion "peek", zeigt in der Regel auf das letzte Byte im Buffer und nicht die aktuelle Position von "ReadLine". Der Einsatz ist relativ einfach, z.B. mit

$reader = [System.IO.File]::OpenText("c:\test.txt")

# oder

$sr = new-object System.IO.Streamreader $stream

Wenn man versucht erst mit einem Filestream zu starten und auf dem dann den Textreader anzuwenden, dann ist der darunterliegende Stream vom Textreader bis zum Ende gelesen worden. Dem Textreader fehlt das "Position"-Property, so dass man selbst eine "Zählung" aufbauen müsste aber der darunterliegende Stream "stimmt" dann auch nicht.

FileStream BinaryReader

Leistungsfähiger ist die FileStream-Klasse, welche auch eine freie Positionierung erlaubt. Allerdings fehlt dieser Klasse die "ReadLine". Man muss also selbst eine Bufferverwaltung bauen, um die Strings zu Lesen und als Zeilen zurück zu geben. Dafür hat diese Klasse eine "Position"-Property. Hier eine ganz einfache Funktion

# open file
$filename="c:\test.txt"
$stream= [System.IO.File]::open($filename, [system.IO.filemode]::open, [System.IO.FileAccess]::Read, [System.IO.Fileshare]::readwrite)
# this basic filestream also support position, seek, read

#Binaryreader allows to read "characters" in addition but not "lines"
$binfile= new-object System.IO.binaryreader $stream
write-host "Position sollte 0 als Start sein :"$binfile.BaseStream.Position
write-host "Ein Zeichen lesen:"$binfile.readchar()
write-host "Position ist nun 1:"$binfile.BaseStream.Position
write-host "ueberspringe 5 Zeichen"
write-host "Next Char lesen ohne pointer zu schieben:"$binfile.peekchar()
$binfile.BaseStream.Seek(5,[System.IO.seekorigin]::current)
write-host "Länge" $binfile.BaseStream.length
$bytearray = $file.readchars(5)
write-host ""[string]::join ("",$bytearray)

Alle "Readxxxx"-Methoden lesen die Binärdaten RAW ein. Ein ReadString ist nicht mit einem ReadLine zu verwechseln, da es erwartet, dass am Anfang die Länge des String steht. Also C++ Denkweise. ReadChars kann mehrere Zeichen lesen und diese als Array liefern, die dann mit "Join". Oder man nutzt direkt den Filestream und baut sich die Pufferverwaltung selbst.

Idealerweise sollte eine Tail-Funktion mit Speicher als auf einem Filestream aufsetzen und Option einen Binaryreader nutzen.

Filestream mit Streamreader kombiniert

Interessant wird der Einsatz der beiden Funktion, indem ein Filestream genutzt wird, um eine Datei zu öffnen und dann an die gewünschte Position zu springen um dann den Stream in einem Streamreader zu verwenden, um bis zum Ende dann mit "ReadLine" die Zeilen einzulesen.

# open file
$filename="c:\test.txt"
$stream= [System.IO.File]::open($filename, [system.IO.filemode]::open, [System.IO.FileAccess]::Read, [System.IO.Fileshare]::readwrite)

# Jump to last position
$stream.Seek(10,[system.io.seekorigin]::begin)

# Start reading
$textstream = new-object System.IO.Streamreader $stream
$teststream.readline()

# Jump back
$stream.Seek(0,[system.io.seekorigin]::begin)
$textstream.DiscardBufferedData()

Wird während des Lesens an die Datei was angehängt, dann verschiebt sich die "Length"-Eigenschaft, aber der Streamreader verharrt mit der "Position" erst mal, bis er mit einem ReadLine am Ende angekommen ist. Dann allerdings ist die Position auch wieder erhöht worden, wobei es nicht sicher ist, ob man dann schon alle Zeilen auch mit ReadLine verarbeitet hat. Hier sollte man am Ende auf jeden Fall Position mit Length abgleichen.

Springt man im Stream mit "Seek" herum, dann bekommt das der Streamreader erst mal nicht mit, da er seine eigene "Bufferverwaltung" hat.. Erst ein "DiscardBufferedData" forciert ein Neueinlesen.

Schade ist, dass der Streamreader nicht auf ein "CR/LF" am Ende wartet. Addiert ein Prozess also eine neue Zeile und ist diese nur "halb" geschrieben, dann wird einmal die bereits geschriebene Zeile und beim nächsten Lesen der zweite Teil als eigene Zeile ausgegeben. Es sei denn der schreibende Prozess "lockt" die Datei anstelle eines einfachen "Append".

Der Einfachheit halber basiert mein Modul auf der dritten Variante und ich erspare mir so umfangreiche Buffer-Routinen um aus einem Binarystream wieder Zeilen zu machen.

Überlegung zur Umsetzung

Eine Umsetzung mit einer aktiven "Watcher-Funktion" ist zwar interessant aber erfordert, dass das Programm quasi "permanent" läuft um auf die Meldungen zu reagieren. Das ist eine Aufgabe für "richtige" Programme und Dienste aber für meinen Ansatz überzogen. Ich kann analog zu Get-USNChanges und GET-ADChanges ganz gut damit leben, wenn ein Skript beim Aufruf die aktuellen Änderungen geeignet ausgibt und sich dann wieder beendet oder nach einer kurzen Wartezeit, die durchaus Sekunden sein können, wieder an die Arbeit macht. Der Vorteil bei dieser Lösung ist die Kompatibilität auch mit UNC-Pfaden und anderen Server, da es nicht auf NTFS-Changenotifications aufsetzen muss.

Technisch soll das Script natürlich eine Liste von Dateien gemäß einem Dateifilter in einem Verzeichnis einlesen und zeilenweise ausgeben. Wenn es die Möglichkeit hat, einen Status zu speichern, dann kann das Script auch beendet und später wieder gestartet werden. Dann soll es dort aufsetzen, wo es beendet wurde. Natürlich muss es einen Weg geben, dem Skript auch nur auf neue Elemente zu warten.

Das Script ist aber definitiv nicht für die Überwachung von Verzeichnissen mit SEHR VIELEN DATEIEN geeignet, da es ja alle Dateien regelmäßig abscannt.

 

Parameter

 

Typ Parameter Default Bedeutung
[string] cookiefilename Kein Cookiefile Das Skript muss, wenn es beendet und später wieder aufgerufen wird, irgendwo den letzten Stand speichern. In der angegebenen Datei landet der Status der überwachten Dateien und die Position.
[string] filter .\*.* Der Filter spezifiziert Pfad und Dateipattern der zu überwachenden Dateien
Ein Cookiefile wird natürlich ausgeschlossen.
[int] sleeptime 3 Sekunden Immer wenn das Skript eine Suche abgeschlossen und die Elemente ausgegeben hat, legt es eine "Pause" ein. Kürzere Pausen belasten das System mehr aber sie können schneller reagieren.
[switch] once $false Wird der Schalter "-once" gesetzt, dann beendet sich das Skript nach einem Durchlauf. Beachten Sie, dass dies nur in Verbindung mit einem Cookie-File Sinn macht. Ansonsten gibt es keine Änderungen zu melden
[switch] skipold $true Weist das Script an, nur neue Änderungen zu melden. Diese Funktion "überstimmt" einen eventuell per Cookiefile eingelesenen alten Status !. Es wird also bei den Dateien zuerst an das Ende gesprungen und dann weiter gelesen. Es werden also keine alten Daten oder zwischenzeitliche Änderungen erkannt.
[switch] nosave $false Mit dem Schalter "-nosave" wird der Cookie am Ende einer Bearbeitung nicht zurück geschrieben. Beim nächsten Aufruf werden also die zuletzt gefundenen Änderungen erneut zurück gegeben. Dies ist primär für Tests geeignet.
[switch] verbose $false Durch die Angabe von "-verbose" werden detailliertere Ausgaben während der Verarbeitung gemacht, die bei eier Fehlersuche helfen können.
[switch] noscreen $false Deaktiviert die zusätzliche Ausgabe auf den Bildschirm der gefundenen Änderungen. Sinnvoll, wenn die normale Ausgabe in die Pipeline nicht mit "| out-null" unterdrückt oder mit einem anderen Prozess weiter verarbeitet wird.
[switch] pipeline no

Steuert die Ausgabe der Daten an die Pipeline zur weiteren Verarbeitung

  • No
    Keine Ausgabe in die Pipeline
  • mini
    Gibt einfach die Zeile ohne weitere Informationen an die Pipeline
  • full
    Es wird ein Datensatz mit den Properties "Timestamp", "Filename", "Line" gemeldet.

 

 

 

Funktionsweise

 

$FileSystemWatcher = New-object System.IO.FileSystemWatcher "c:\temp"
$result = $FileSystemWatcher.WaitForChanged("all")

 

 

 

 

Erkenne "gekürzte Dateien"

Erkenne gelöschte Dateien

Erkenne neue Dateien

 

Schnittstelle: Powershell Commandlet oder allgemeine COM und .NET-Klasse

Wer sich hier heranwagen will, sollte sich etwas mit C# auskennen und überlegen, welche Parameter an die Powershell übergeben werden müssen. Es muss auch nicht unbedingt ein Powershell-Commandlet sein. Es kann auch durchaus eine generische .NET Klasse sein, die man ebenso einfach per Powershell instanzieren kann, z.B.:

$tail = New-Object msxfaq.tail
$tail.filefilter = c:\test\*.log  ' definiere überwachtes Verzeichnis\Datei(en)
$tail.statusstore = filename  ' Datei, zur Speicherung des Status
$tail.setfilter ( array of regex)  ' Filter für relevante Zeilen hinterlegen
$tail.savestatus ' Aktuellen Status sichern, d.h. nach erfolgter Verarbeitung
line = $tail.getline   ' Hole nächste Zeile und Quelle ab.

Wäre die .NET Klasse auch als COM-Objekt zu erreichen, könnte man sie auch aus VBScript einfach verwenden:

#### Beispiel für VBScript (COM-Komponente)
 
' Beispiel:
Set Tail = CreateObject("msxfaq.tail")   ' Instanziere Klasse
Tail.filefilter = c:\test\*.log  ' definiere überwachtes Verzeichnis\Datei(en)
Tail.statusstore = filename  ' Datei, zur Speicherung des Status
Tail.setfilter ( array of regex)  ' Filter für relevante Zeilen hinterlegen
Tail.savestatus ' Aktuellen Status sichern, d.h. nach erfolgter Verarbeitung
line = tail.getline (timeout)   ' Hole nächste Zeile und Quelle ab.

Die verschiedenen Methoden sind natürlich nur ein Vorschlag. 

Sonstiger Einsatz

Natürlich ist der Einsatz eines "Tail" auf Dateien nur ein erster Ansatz. Es gibt viele anderen "Logs," die man so einfach auswerten kann. Es gibt Tools, die z.B. Eventlogs oder AD-Änderungen in Textdateien schreiben können und damit anderen Diensten zugänglich machen. Dies kostet meist weniger Ressourcen, als bei jedem Aufruf z.B. im Eventlog an die letzte Position zu springen und weiter zu verarbeiten. Hier kann Get-Tail also der Schlüssel für eine weitere effektive Verarbeitung sein.

Weitere Links

Keywords:Tail Ideen