powershell 从System.IO命名空间中的FileStream而不是Get-Content -Tail

p1tboqfb  于 2023-03-23  发布在  Shell
关注(0)|答案(4)|浏览(155)

我想使用System.IO命名空间中的FileStream而不是Get-Content cmdlet。如何操作?
谢谢,

$fromaddress = "filemon@contoso.com"
$emailto = "IT@contoso.com"
$SMTPSERVER = "xx.xx.xx.xx"
$global:FileChanged = $false 
$folder = "C:\temp" 
$filter = "log.log" 
$watcher = New-Object IO.FileSystemWatcher $folder,$filter -Property @{ IncludeSubdirectories = $false EnableRaisingEvents = $true }
Register-ObjectEvent $Watcher "Changed" -Action {$global:FileChanged = $true} > $null

while ($true)
{ 
    while ($global:FileChanged -eq $false){ 
        Start-Sleep -Milliseconds 100 
    }

    if($(Get-Content -Tail 1 -Path $folder\$filter).Contains("Finished."))
    {
        Send-mailmessage -from $fromaddress -to $emailto -subject "Log changed" -smtpServer $SMTPSERVER -Encoding UTF8 -Priority High
    }

    # reset and go again
    $global:FileChanged = $false
}

编辑1:

$fromaddress = "filemon@contoso.com"
$emailto = "IT@contoso.com"
$SMTPSERVER = "xx.xx.xx.xx"
$global:FileChanged = $false 
$folder = "C:\tmp" 
$filter = "log.log" 
$watcher = New-Object IO.FileSystemWatcher $folder,$filter -Property @{ IncludeSubdirectories = $false ; EnableRaisingEvents = $true }
Register-ObjectEvent $Watcher "Changed" -Action {$global:FileChanged = $true} > $null

function Read-LastLine ([string]$Path) {
    # construct a StreamReader object
    $reader   = [System.IO.StreamReader]::new($path)
    $lastLine = ''
    while($null -ne ($line = $reader.ReadLine())) {
        $lastLine = $line
    }

    # clean-up the StreamReader
    $reader.Dispose()

    # return the last line of the file
    $lastLine
}

while ($true)
{ 
    while ($global:FileChanged -eq $false){ 
        Start-Sleep -Milliseconds 100 
    }
    
    $logFile = $Event.SourceEventArgs.FullPath

    if ((Read-LastLine -Path $logFile) -match "Finished.")
    {
       write-host "mail sending"
        Send-mailmessage -from $fromaddress -to $emailto -subject "Log changed" -smtpServer $SMTPSERVER -Encoding UTF8 -Priority High
    }

    # reset and go again
    $global:FileChanged = $false
}

留言:

MethodInvocationException: C:\monfile.ps1:15
Line |
  15 |      $reader   = [System.IO.StreamReader]::new($path)
     |      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Exception calling ".ctor" with "1" argument(s): "The value cannot be an empty string. (Parameter 'path')"
InvalidOperation: C:\monfile.ps1:17
Line |
  17 |      while($null -ne ($line = $reader.ReadLine())) {
     |                       ~~~~~~~~~~~~~~~~~~~~~~~~~~
     | You cannot call a method on a null-valued expression.
InvalidOperation: C:\monfile.ps1:22
Line |
  22 |      $reader.Dispose()
     |      ~~~~~~~~~~~~~~~~~
     | You cannot call a method on a null-valued expression.
dauxcl2d

dauxcl2d1#

不确定这是否会比使用Get-Content -Tail 1更快,但您可以创建一个帮助函数,使用StreamReader返回文件中的最后一行。

编辑

当事件发生时,您的代码并没有真正响应该事件,但似乎依赖于一个名为FileChanged的全局变量,您在while循环中测试了该变量。
FileSystemWatcher的神奇之处在于,如果log.log文件中发生了更改,您可以坐下来休息,直到所选事件触发。只有当这样的事件发生时,才会执行Action参数中定义的脚本块,因此这里使用helper函数重写代码以读取日志文件的最后一行。
(自从我早期的回答,我已经改变了该函数,只检索最后一行,不是空的或空白)

function Read-LastLine {
    [CmdletBinding()]
    param (
        [ValidateNotNullOrEmpty()]
        [Alias('FullName')]
        [string]$Path
    )
    # construct a StreamReader object
    $reader   = [System.IO.StreamReader]::new($path)
    $lastLine = [string]::Empty
    while($null -ne ($line = $reader.ReadLine())) {
        # do not store this line if it is empty or whitespace-only
        if (![string]::IsNullOrWhiteSpace($line)) {
            $lastLine = $line
        }
    }

    # clean-up the StreamReader
    $reader.Dispose()

    # return the last line of the file
    $lastLine
}

# create a splatting Hashtable for the Send-MailMessage cmdlet
$mailParams = @{
    From       = "filemon@contoso.com"
    To         = "IT@contoso.com"
    SmtpServer = "xx.xx.xx.xx"
    Subject    = "Log changed"
    Encoding   = 'UTF8'
    Priority   = 'High'
}

# define parameters for the FileSystemWatcher
$folder = 'C:\tmp'
$filter = 'log.log'

# to prevent the FileSystemWatcher to fire events twice, keep track of the last time
$LastEventTime = (Get-Date)

# define the Action scriptblock for the FileSystemWatcher
# this code which will be executed every time a file change is detected
$action = {
    # try to detect if this event fired twice
    if (($Event.TimeGenerated.Ticks - $script:LastEventTime.Ticks) -gt 100000) {
        $logFile = $Event.SourceEventArgs.FullPath
        if ((Read-LastLine -Path $logFile) -match "Finished") {
            $script:LastEventTime = $Event.TimeGenerated
            Write-Host "mail sending"
            Send-mailmessage @mailParams
        }
    }
}

# create the FileSystemWatcher and register the event
$watcher = New-Object IO.FileSystemWatcher $folder, $filter -Property @{
    IncludeSubdirectories = $false
    EnableRaisingEvents   = $true
    NotifyFilter          = [IO.NotifyFilters]::LastWrite
}
Register-ObjectEvent $Watcher -EventName 'Changed' -SourceIdentifier 'LogChanged' -Action $action

当不再需要监视文件时,请注销事件并释放监视器

Unregister-Event -SourceIdentifier 'LogChanged'
$watcher.Dispose()
b91juud3

b91juud32#

尝试以下操作:

$SourceStream = [System.IO.FileStream]::new('c:\temp\test.txt',[System.IO.FileMode]::Open);
for($pos = $SourceStream.Length - 1; $pos -ge 0; $pos--)
{
   $SourceStream.Position = $pos
   $b = $SourceStream.ReadByte()
   if(($b -eq 0x0A) -or ($b -eq 0x0D)) { break;}
}
$length = $SourceStream.Length - $pos
$buffer = New-Object byte[]($length)
$SourceStream.Read([byte[]]$buffer, 0, $length)
$tail = [System.Text.Encoding]::UTF8.GetString($buffer, 0, $length)
Write-Host $tail
b4qexyjb

b4qexyjb3#

IO.FileSystemWatcher版本:

在查找前面答案的参考文献时,我发现我创建的LogFileReader类基于C#代码How to read a log file which is hourly updated in c#?,它与原始代码中的代码非常相似。
这个LogFileWatcher类类似于前面的LogFileReader类,但只保留了基本的内容,并使用了一个Dirty()方法来指示何时有更多的行可用:

class LogFileWatcher: IDisposable {
    [IO.StreamReader]$_streamReader
    [int]$_position
    hidden [void]Initialize([string]$logFile, [Text.Encoding]$encoding) {
        $this._position = 0
        $this._streamReader = if ([IO.File]::Exists($logFile)) {
            [IO.StreamReader]::new([IO.FileStream]::new( $logFile, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite), $encoding)
        } else {$null}
    }
    LogFileWatcher([string]$logFile) { $this.Initialize($logFile, [Text.Encoding]::UTF8) }
    LogFileWatcher([string]$logFile, [Text.Encoding]$encoding) { $this.Initialize($logFile, $encoding) }
    [void]GoToEnd() { if($this._streamReader){$this._position = $this._streamReader.BaseStream.Length} }
    [bool]Dirty() {
        return $this._position -ne $this._streamReader.BaseStream.Length
    }
    [string[]]ReadLines() {
        $Lines = @()
        if($this._streamReader -and $this.Dirty()) {
            $null = $this._streamReader.BaseStream.Seek($this._position, [IO.SeekOrigin]::Begin)
            while ($null -ne ($Line = $this._streamReader.ReadLine())) {if($Line){$Lines += $Line}}
            $this._position = $this._streamReader.BaseStream.Position
        }
        return $Lines
    }
    [void]Dispose() {
        try{
            [IO.FileStream]$FileStream = $this._streamReader.BaseStream
            $this._streamReader.Dispose()
            $FileStream.Dispose()
        }
        finally{}
    }
}

示例使用,C#代码的混合体,以及前面回答中的代码:

$fromaddress = "filemon@contoso.com"
$emailto = "IT@contoso.com"
$SMTPSERVER = "xx.xx.xx.xx"
$folder = "C:\tmp"
$filter = "log.log"

$LogFileWatcher = [LogFileWatcher]::new("$folder\$filter", [Text.Encoding]::UTF8)
$LogFileWatcher.GoToEnd()

#[console]::TreatControlCAsInput = $true
while ($true) {
    Start-Sleep -Milliseconds 100
    #if ($Host.UI.RawUI.KeyAvailable -and (3 -eq [int]$Host.UI.RawUI.ReadKey("AllowCtrlC,IncludeKeyUp,NoEcho").Character)) {break}
    if($LogFileWatcher.Dirty) {
        foreach ($Line in $LogFileWatcher.ReadLines()) {
            if ($Line -match "Finished.") {
                write-host "mail sending"
                Send-mailmessage -from $fromaddress -to $emailto -subject "Log changed" -smtpServer $SMTPSERVER -Encoding UTF8 -Priority High
            }
        }
    }
}
#$LogFileWatcher.Dispose()
#Write-Host "That's all Folks!"

另外,如果我没记错的话,是What's the least invasive way to read a locked file in C# (perhaps in unsafe mode)?给了我线索,最终找到了上面的代码链接。

ajsxfq5m

ajsxfq5m4#

在阅读日志文件的最后一行时会遇到一个真实的的问题,那就是你怎么知道它一次只添加了一行或一个日志条目?你真正需要的是跟踪日志文件并在添加时只读取新内容的东西。我的解决方案是创建一个LogFileReader类。
在我创建的一个测试脚本中,我在脚本的顶部添加了以下几行代码,这些代码取自问题:

$fromaddress = "filemon@contoso.com"
$emailto = "IT@contoso.com"
$SMTPSERVER = "xx.xx.xx.xx"
$global:FileChanged = $false 
$folder = "C:\tmp" 
$filter = "log.log" 
$watcher = New-Object IO.FileSystemWatcher $folder,$filter -Property @{ IncludeSubdirectories = $false ; EnableRaisingEvents = $true }
Register-ObjectEvent $Watcher "Changed" -Action {$global:FileChanged = $true} > $null

然后添加了这个实验性的LogFileReader类。我在过去几周的某个时候创建了这个类来解决这个问题。到目前为止,它运行得很好,我没有看到它抛出错误或有问题。但同样,它是实验性的,没有彻底测试。
对于此类:
1.使用文件的完整路径创建一个示例,您可以选择添加编码。如果没有提供编码,则UTF8是默认值。
1.如果您想在阅读新行之前读取整个文件,则调用ReadLines()ReadText(),否则,调用GoToEnd()以跳过文件的当前内容。

  1. ReadLines()返回一个字符串数组。如果你想一次一个地获取每一个新添加的行,使用这个。
  2. ReadText()返回单个字符串。如果您希望所有新添加的内容作为单个字符串,请使用此。
    1.完成后,使用Dispose()释放StreamReader。**编辑:**将旧的Dispose()方法替换为新方法,该方法从StreamReader的BaseStream属性中检索FileStream,释放StreamReader,然后释放FileStream。
    1.如果出于某种原因,在多次读取之后,您希望从头读取文件,请调用GoToStart(),然后下一次读取将获得整个文件的内容。
    1.这个类维护一个指向上次读取结束的指针,并且每次调用ReadLines()时,指针都会向前移动到当前读取的结束。
class LogFileReader: IDisposable {
    [string]$_logFile
    [int]$_position
    [IO.StreamReader]$_streamReader
    hidden [void]Initialize([string]$logFile, [Text.Encoding]$encoding) {
        $this._logFile = $logFile
        $this._position = 0
        $this._streamReader = if ([IO.File]::Exists($this._logFile)) {
            [IO.StreamReader]::new([IO.FileStream]::new( $logFile, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite), $encoding)
        } else {$null}
    }
    LogFileReader([string]$logFile) { $this.Initialize($logFile, [Text.Encoding]::UTF8) }
    LogFileReader([string]$logFile, [Text.Encoding]$encoding) { $this.Initialize($logFile, $encoding) }
    [void]GoToStart() { $this._position = 0 }
    [void]GoToEnd() { if($this._streamReader){$this._position = $this._streamReader.BaseStream.Length} }
    [string[]]ReadLines() {
        $Lines = @()
        if($this._streamReader) {
            $BaseLength = $this._streamReader.BaseStream.Length
            if ( $this._position -ne $BaseLength ) {
                if($this._position -gt $BaseLength ) { $this.GotoStart() }
                $null = $this._streamReader.BaseStream.Seek($this._position, [IO.SeekOrigin]::Begin)
                while ($null -ne ($Line = $this._streamReader.ReadLine())) {if($Line){$Lines += $Line}}
                $this._position = $this._streamReader.BaseStream.Position
            }
        }
        return $Lines
    }
    [string]ReadText() {
        $Return = ''
        $this.ReadLines() | ForEach-Object {$Return += "$_$([System.Environment]::NewLine)"}
        return $Return
    }
    [void]Dispose() {
        try{
            [IO.FileStream]$FileStream = $this._streamReader.BaseStream
            $this._streamReader.Dispose()
            $FileStream.Dispose()
        }
        finally{}
    }
}

下面的测试代码被添加到脚本的末尾。**编辑:**它现在已经注解掉了允许捕获Ctrl+C和Gracefully stopping in Powershell的代码。显然,[console]::TreatControlCAsInput在所有情况下都不可用,即使docs似乎没有给予足够的信息来解释它何时不可用。它还包括3个版本的新内容的阅读。一个foreach {}版本与if用于捕获“Finished.”行,以及2个注解掉的版本以给出示例用途。
注解掉了Send-mailmessage行,并添加了一个额外的Write-Host "[$Line]"用于测试目的。

$LogFileReader = [LogFileReader]::new("$folder\$filter", [Text.Encoding]::UTF8)
$LogFileReader.GoToEnd()

#[console]::TreatControlCAsInput = $true
:TrappedForever while ($true) {
    while ($global:FileChanged -eq $false){
        Start-Sleep -Milliseconds 100
        #if ($Host.UI.RawUI.KeyAvailable -and (3 -eq [int]$Host.UI.RawUI.ReadKey("AllowCtrlC,IncludeKeyUp,NoEcho").Character)) {break TrappedForever}
    }
    foreach ($Line in $LogFileReader.ReadLines()) {
        Write-Host "[$Line]"
        if ($Line -match "Finished.") {
            write-host "mail sending"
            #Send-mailmessage -from $fromaddress -to $emailto -subject "Log changed" -smtpServer $SMTPSERVER -Encoding UTF8 -Priority High
        }
    }
    #$LogFileReader.ReadLines() | ForEach-Object {Write-Host "[$_]"}
    #Write-Host ($LogFileReader.ReadText()) -NoNewline

    # reset and go again
    $global:FileChanged = $false
}
$watcher.EnableRaisingEvents = $false
$watcher.Dispose()
$LogFileReader.Dispose()
Write-Host "All Done!"

相关问题