Private/FileMonitor.psm1

#!/usr/bin/env pwsh
using namespace System
using namespace System.IO

using module ./Enums.psm1
using module ./Exceptions.psm1

#Requires -Modules PsModuleBase

class FileMonitor {
  static [bool] $FileClosed = $true
  static [bool] $FileLocked = $false
  static [ConsoleKeyInfo[]] $Keys = @()
  static [ValidateNotNull()][IO.FileInfo] $FileTowatch
  static [ValidateNotNull()][string] $LogvariableName = $(if ([string]::IsNullOrWhiteSpace([FileMonitor]::LogvariableName)) {
      $n = ('fileMonitor_log_' + [guid]::NewGuid().Guid).Replace('-', '_');
      Set-Variable -Name $n -Scope Global -Value ([string[]]@()); $n
    } else {
      [FileMonitor]::LogvariableName
    }
  )
  static [FileSystemWatcher] MonitorFile([string]$File) {
    return [FileMonitor]::monitorFile($File, { Write-Host "[+] File monitor Completed" -ForegroundColor Green })
  }
  static [FileSystemWatcher] MonitorFile([string]$File, [scriptblock]$Action) {
    [ValidateNotNull()][FileInfo]$File = [IO.FileInfo][PsModuleBase]::GetUnResolvedPath($File)
    if (![IO.File]::Exists($File.FullName)) {
      throw "The file does not exist"
    }
    [FileMonitor]::FileTowatch = $File
    $watcher = [FileSystemWatcher]::new();
    $Watcher = New-Object IO.FileSystemWatcher ([IO.Path]::GetDirectoryName($File.FullName)), $File.Name -Property @{
      IncludeSubdirectories = $false
      EnableRaisingEvents   = $true
    }
    $watcher.Filter = $File.Name
    $watcher.NotifyFilter = [NotifyFilters]::LastWrite;
    $onChange = Register-ObjectEvent $Watcher Changed -Action {
      [FileMonitor]::FileLocked = $true
    }
    $OnClosed = Register-ObjectEvent $Watcher Disposed -Action {
      [FileMonitor]::FileClosed = $true
    }
    # [Console]::Write("Monitoring changes to $File"); [Console]::WriteLine("Press 'crl^q' to stop")
    do {
      try {
        [FileMonitor]::FileLocked = [FileMonitor]::IsFileLocked($File.FullName)
      } catch [IOException] {
        [FileMonitor]::FileLocked = $(if ($_.Exception.Message.Contains('is being used by another process')) {
            $true
          } else {
            throw 'An error occured while checking the file'
          }
        )
      } finally {
        [Threading.Thread]::Sleep(100)
      }
    } until ([FileMonitor]::FileClosed -and ![FileMonitor]::FileLocked -and ![FileMonitor]::IsFileOpenInVim($File.FullName))
    Invoke-Command -ScriptBlock $Action
    Unregister-Event -SubscriptionId $onChange.Id; $onChange.Dispose();
    Unregister-Event -SubscriptionId $OnClosed.Id; $OnClosed.Dispose(); $Watcher.Dispose();
    return $watcher
  }
  static [PsObject] MonitorFileAsync([string]$filePath) {
    # .EXAMPLE
    # $flt = [FileMonitor]::MonitorFileAsync($filePath)
    # $flt.Thread.CloseInputStream();
    # $flt.Thread.StopJobAsync();
    # Stop-Job -Name $flt.Name -Verbose -PassThru | Remove-Job -Force -Verbose
    # $flt.Thread.Dispose()MOnitorFile
    # while ((Get-Job -Name $flt.Name).State -ne "Completed") {
    # # DO other STUFF here ...
    # }
    $threadscript = [scriptblock]::Create("[FileMonitor]::MonitorFile('$filePath')")
    $fLT_Name = "kLThread-$([guid]::NewGuid().Guid)"
    return [PSCustomObject]@{
      Name   = $fLT_Name
      Thread = Start-ThreadJob -ScriptBlock $threadscript -Name $fLT_Name
    }
  }
  static [string] GetLogSummary() {
    return [FileMonitor]::GetLogSummary([FileMonitor]::LogvariableName)
  }
  static [string] GetLogSummary([string]$LogvariableName) {
    [ValidateNotNullOrWhiteSpace()][string]$LogvariableName = $LogvariableName
    $l = Get-Variable -Name $LogvariableName -Scope Global -ValueOnly;
    $summ = ''; $rgx = "\[.*\] The file '.*' is open in nvim \(PID: \d+\)"
    if ($null -eq $l) { return '' }; $ct = $l.Where({ $_ -notmatch $rgx })
    $LogSessions = @();
    $LogSessions += $(if ($ct.count -gt 1) {
        $(($l.ForEach({ if ($_ -notmatch $rgx) { $_ + '|' } else { $_ } })) -join "`n").Split('|')
      } else {
        [string]::Join("`n", $l)
      }
    )
    foreach ($item in $LogSessions) {
      $s = ''; $lines = $item.Split("`n")
      0 .. $lines.Count | ForEach-Object { if ($_ -eq 0) { $s += "$($lines[0])`n" } elseif ($lines[$_] -match $rgx -or $lines[$_ + 1] -match $rgx) { $s += '.' } else { $s += "`n$($lines[$_ - 1])" } }
      $summ += [string]::Join("`n", $s.Split("`n").ForEach({ if ($_ -like "......*") { '?' } else { $_ } })).Trim()
      $summ += "`n"
    }
    return $summ.Trim()
  }
  static [bool] IsFileOpenInVim([FileInfo]$file) {
    $res = $null; $logvar = Get-Variable -Name ([FileMonitor]::LogvariableName) -Scope Global;
    $fileName = Split-Path -Path $File.FullName -Leaf;
    $res = $false; $_log_msg = @(); $processes = Get-Process -Name "nvim*", "vim*" -ErrorAction SilentlyContinue
    foreach ($process in $processes) {
      if ($process.CommandLine -like "*$fileName*") {
        $_log_msg = "[{0}] The file '{1}' is open in {2} (PID: {3})" -f [DateTime]::Now.ToString(), $fileName, $process.ProcessName, $process.Id
        $res = $true; continue
      }
    }
    $_log_msg = $_log_msg -join [Environment]::NewLine
    if ([string]::IsNullOrEmpty($_log_msg)) {
      $res = $false; $_log_msg = "[{0}] The file '{1}' is not open in vim" -f [DateTime]::Now.ToString(), $fileName
    }
    $logvar.Value += $_log_msg
    Set-Variable -Name ([FileMonitor]::LogvariableName) -Scope Global -Value $logvar.Value | Out-Null
    return $res
  }
  static [bool] IsFileLocked([string]$filePath) {
    $res = $true; $logvar = Get-Variable -Name ([FileMonitor]::LogvariableName) -Scope Global; $filePath = Resolve-Path -Path $filePath -ErrorAction SilentlyContinue
    try {
      # (lsof -t "$filePath" | wc -w) -gt 0
      [FileStream]$stream = [IO.File]::Open($filePath, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::None)
      if ($stream) { $stream.Close(); $stream.Dispose() }
      $res = $false
    } finally {
      if ($res) { $logvar.Value += "[$([DateTime]::Now.ToString())] File is already locked by another process." }
      Set-Variable -Name ([FileMonitor]::LogvariableName) -Scope Global -Value $logvar.Value | Out-Null
    }
    return $res
  }
}