I am having an issue with IO.FileSystemWatcher. I have a script that when file copies to a certain directory it processes that file and copies it to another folder on network. But there is a problem, it starts copying as soon as a fsw fires oncreate event, which generates an error (file is open) and I want it to start only after file has finished copying. Rename and delete works properly.
cloud.ps1 is script that processes file and copies it.
This is the code for monitor script:
$watch = '\\HR-ZAG-SR-0011\ACO\ACO2\99. BOX'
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $watch
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
$changed = Register-ObjectEvent $watcher "Changed" -Action {
# Write-Host "Changed: $($eventArgs.ChangeType)"
# $watch\cloud.ps1 modify "$($eventArgs.FullPath)"
}
$created = Register-ObjectEvent $watcher "Created" -Action {
Write-Host "Created: $($eventArgs.FullPath)"
.\cloud.ps1 create "$($eventArgs.FullPath)"
}
$deleted = Register-ObjectEvent $watcher "Deleted" -Action {
Write-Host "Deleted: $($eventArgs.FullPath)"
.\cloud.ps1 delete "$($eventArgs.FullPath)"
}
$renamed = Register-ObjectEvent $watcher "Renamed" -Action {
Write-Host "Renamed: $($eventArgs.OldFullPath) to $($eventArgs.FullPath)"
.\cloud.ps1 rename "$($eventArgs.OldFullPath)" "$($eventArgs.FullPath)"
}
You can put in a loop to start checking to see if you can get a write lock on the file:
While ($True)
{
Try {
[IO.File]::OpenWrite($file).Close()
Break
}
Catch { Start-Sleep -Seconds 1 }
}
As long as the file is being written to, you won't be able to get a write lock, and the Try will fail. Once the write is complete and the file is closed, the OpenWrite will succeed, the loop will break and you can proceed with copying the file.
Related
Am I missing something?
I can start the debug process with F5, but I cannot end it, and I cannot step through code or do normal debugging.
I assume this is due to the fact that the code is hanging off Register-ObjectEvent ?
(Watching a file system event....)
What is the method to run this code and keep the debugger attached to what is going on?
The code:
$folder_to_watch = 'C:\Users\demouser\Downloads\'
$file_name_filter = '*.aac'
# to archive .aac files
$destination = 'c:\temp\test\arc\'
$DestinationDirMP3 = 'C:\data\personal\hinative-mp3'
$Watcher = New-Object IO.FileSystemWatcher $folder_to_watch, $file_name_filter -Property #{
IncludeSubdirectories = $false
NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'
}
$VLCExe = 'C:\Program Files\VideoLAN\VLC\vlc.exe'
$onCreated = Register-ObjectEvent $Watcher -EventName Created -SourceIdentifier FileCreated -Action {
$path = $Event.SourceEventArgs.FullPath
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$timeStamp = $Event.TimeGenerated
Write-Host "The file '$name' was $changeType at $timeStamp"
Write-Host $path
# File Checks
while (Test-LockedFile $path) {
Start-Sleep -Seconds .2
}
# Move File
Write-Host "moving $path to $destination"
Move-Item $path -Destination $destination -Force -Verbose
# build the path to the archived .aac file
$SourceFileName = Split-Path $path -Leaf
$DestinationAACwoQuotes = Join-Path $destination $SourceFileName
$DestinationAAC = "`"$DestinationAACwoQuotes`""
$MP3FileName = [System.IO.Path]::ChangeExtension($SourceFileName,".mp3")
$DestinationMP3woQuotes = Join-Path $DestinationDirMP3 $MP3FileName
$DestinationMP3 = "`"$DestinationMP3woQuotes`""
$VLCArgs = "-I dummy -vvv $DestinationAAC --sout=#transcode{acodec=mp3,ab=48,channels=2,samplerate=32000}:standard{access=file,mux=ts,dst=$DestinationMP3} vlc://quit"
Write-Host "args $VLCArgs"
Start-Process -FilePath $VLCExe -ArgumentList $VLCArgs
}
function Test-LockedFile {
param ([parameter(Mandatory=$true)][string]$Path)
$oFile = New-Object System.IO.FileInfo $Path
if ((Test-Path -Path $Path) -eq $false)
{
return $false
}
try
{
$oStream = $oFile.Open([System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
if ($oStream)
{
$oStream.Close()
}
$false
}
catch
{
# file is locked by a process.
return $true
}
}
From the official documentation of Register-ObjectEvent (notes section)
Events, event subscriptions, and the event queue exist only in the current session. If you close the current session, the event queue is discarded and the event subscription is canceled.
Everything above belong to your session, which is terminated when the process exit. Even if it was somewhere else, your .net FileSystemWatcher is part of that process and is terminated as soon your session exit.
Now, when you debug through VSCode / ISE, your session is created beforehand and does not terminate after you exit the script, which allow you to evaluate variable of the last execution. It also mean your subscriptions and event callbacks and the associated .net objects remain in memory and active at this time.
That being said, your debugger also detached at the moment your script exited. It also mean that if you were not debugging and just running the script, your session would exit immediately after the script execution and thus, your listener, events callback and everything else would immediately exit without a chance to process anything.
To keep the debugger attached, be able to debug and also to have the script working in a normal context at all, you need to somewhat keep the process alive.
The usual way to ensure that is to add a loop at the end of the script.
# At the end of the script.
while ($true) {
Start-Sleep -Seconds 1
}
Note that events are raised to the main thread, which mean that if your script is sleeping, it won't be processed immediately. Therefore, in the example above, if 10 events were to occurs within the 1 second period, they would all get processed at the same time, when the thread stop sleeping.
Debugging note:
To deal with event already registered error during debugging Register-ObjectEvent : Cannot subscribe to the specified event. A subscriber with the source identifier 'FileCreated' already exists.., you can add cleanup code in the Finally part of a Try / Catch / Finally block.
try {
# At the end of the script...
while ($true) {
Start-Sleep -Seconds 1
}
}
catch {}
Finally {
# Work with CTRL + C exit too !
Unregister-Event -SourceIdentifier FileCreated
}
References:
Register-ObjectEvent
We have a convoluted solution to some printing issues (caused by citrix and remote servers). Basically from the main server, we force push a pdf file to the remote pc and then have a powershell script which constantly runs on the remote pc to "catch" the file and push it to the local printer
This works "fine"
However we get random dropouts. The powershell script doesn't seem to have crashed because it's still running in windows but the actual action doesn't seem to be processing new files
I have done A LOT of reading today and there's mention of having to name and unregister events when you're done otherwise it can cause a buffer overflow issues and make powershell stop processing the action. But I'm unsure where it should actually go within the code. The idea is that this script will run permanently, so do we unregister or remove the event within the action itself or somewhere else?
I previous had A LOT of dummy logging going on within the action to try to find where it failed, but it seems to stop at different points without any justifiable reason (ie, it would fail at the command to find files, other times at the command to move etc etc)
### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "l:\files\cut"
$watcher.Filter = "*.pdf"
$watcher.IncludeSubdirectories = $false
$watcher.EnableRaisingEvents = $true
### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
$action = { $path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$scandir="l:\files\cut"
$scanbackdir="l:\files\cut\back"
$scanlogdir="l:\files\cut\log"
$sumatra="l:\SumatraPDF.exe"
$pdftoprint=""
$printername= "MainLBL"
### Get the List of files in the Directory, print file, wait and then move file
Get-ChildItem -Path $scandir -filter "*.pdf" -Name | % {
$pdftoprint=$_
& $sumatra -silent $scandir\$pdftoprint -print-to $printername
sleep 3
Move-Item -force $scandir\$pdftoprint $scanbackdir
}
}
### Define what happens when script fails
$erroraction = {echo $(get-date) the process crashed | Out-File -Append l:\files\cut\log\errorlog.txt}
### DECIDE WHICH EVENTS SHOULD BE WATCHED
Register-ObjectEvent $watcher "Error" -Action $erroraction
Register-ObjectEvent $watcher "Created" -Action $action
while ($true) {sleep 5}
If you want a script to run in the background, then look to PowerShell background jobs.
If you want a script to run permanently, then you want to make it a service ...
See these:
How to Create a User-Defined Service
How to run a PowerShell script as a Windows service
... or attach that to a scheduled task, that would restart it on reboots.
There are two ways to implement a FileSystemWatcher.
Synchronous
Asynchronous
A synchronous FileSystemWatcher, by it nature, when a change is detected, control is returned to your script so it can process the change. If another file change occurs while your script is no longer waiting for events, it gets lost. Hence leading to unexpected outcomes.
Using the FileSystemWatcher Asynchronously, it would continue to log new filesystem changes and process them once PowerShell is done processing previous changes.
* An Sample - Example Asynchronous FileSystemWatcher*
### New-FileSystemWatcherAsynchronous
# Set the folder target
$PathToMonitor = Read-Host -Prompt 'Enter a folder path'
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher
$FileSystemWatcher.Path = $PathToMonitor
$FileSystemWatcher.IncludeSubdirectories = $true
# Set emits events
$FileSystemWatcher.EnableRaisingEvents = $true
# Define change actions
$Action = {
$details = $event.SourceEventArgs
$Name = $details.Name
$FullPath = $details.FullPath
$OldFullPath = $details.OldFullPath
$OldName = $details.OldName
$ChangeType = $details.ChangeType
$Timestamp = $event.TimeGenerated
$text = "{0} was {1} at {2}" -f $FullPath, $ChangeType, $Timestamp
Write-Host $text -ForegroundColor Green
# Define change types
switch ($ChangeType)
{
'Changed' { "CHANGE" }
'Created' { "CREATED"}
'Deleted' { "DELETED"
# Set time intensive handler
Write-Host "Deletion Started" -ForegroundColor Gray
Start-Sleep -Seconds 3
Write-Warning -Message 'Deletion complete'
}
'Renamed' {
$text = "File {0} was renamed to {1}" -f $OldName, $Name
Write-Host $text -ForegroundColor Yellow
}
default { Write-Host $_ -ForegroundColor Red -BackgroundColor White }
}
}
# Set event handlers
$handlers = . {
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Changed -Action $Action -SourceIdentifier FSChange
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Created -Action $Action -SourceIdentifier FSCreate
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Deleted -Action $Action -SourceIdentifier FSDelete
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Renamed -Action $Action -SourceIdentifier FSRename
}
Write-Host "Watching for changes to $PathToMonitor" -ForegroundColor Cyan
try
{
do
{
Wait-Event -Timeout 1
Write-Host '.' -NoNewline
} while ($true)
}
finally
{
# End script actions + CTRL+C executes the remove event handlers
Unregister-Event -SourceIdentifier FSChange
Unregister-Event -SourceIdentifier FSCreate
Unregister-Event -SourceIdentifier FSDelete
Unregister-Event -SourceIdentifier FSRename
# Remaining cleanup
$handlers |
Remove-Job
$FileSystemWatcher.EnableRaisingEvents = $false
$FileSystemWatcher.Dispose()
Write-Warning -Message 'Event Handler completed and disabled.'
}
I have not encountered a script that will run permanently on windows.
So with that in mind we take it for granted that some issue beyond your control such as the network or power or a system shutdown will occur.
With that in mind we have a lifecycle for this script and everything should be properly cleaned up at the end. In this case we have while loop that should theoretically never end however if an exception is thrown it will end. Within the while loop if any of the events have been deregistered we can reregister them. If the watcher has been disposed we can recreate it and the events. If this really is mission critical code, then I would look at .net as an alternative with something like hangfire with nlog as a windows service.
### WRAP Everything in a try finally so we dispose of events
try {
### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$watcherArgs = #{
Path = "l:\files\cut"
Filter = "*.pdf"
IncludeSubdirectories = $false
EnableRaisingEvents = $true
}
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $watcherArgs.Path
$watcher.Filter = $watcherArgs.Filter
$watcher.IncludeSubdirectories = $watcherArgs.IncludeSubdirectories
$watcher.EnableRaisingEvents = $watcherArgs.EnableRaisingEvents
### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
$action = { $path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$scandir="l:\files\cut"
$scanbackdir="l:\files\cut\back"
$scanlogdir="l:\files\cut\log"
$sumatra="l:\SumatraPDF.exe"
$pdftoprint=""
$printername= "MainLBL"
### Get the List of files in the Directory, print file, wait and then move file
Get-ChildItem -Path $scandir -filter "*.pdf" -Name | % {
$pdftoprint=$_
if($LASTEXITCODE -ne 0) {
# Do something
# Reset so we know when sumatra fails
$LASTEXITCODE = 0
}
& $sumatra -silent $scandir\$pdftoprint -print-to $printername
if($LASTEXITCODE -ne 0) {
# Do something to handle sumatra
}
sleep 3
# Split up copy and delete so we never loose files
[system.io.file]::Copy("$scandir\$pdftoprint", "$scanbackdir", $true)
[system.io.file]::Delete("$scandir\$pdftoprint")
}
}
### Define what happens when script fails
$erroraction = {
echo "$(get-date) the process crashed" | Out-File -Append "l:\files\cut\log\errorlog.txt"
}
### DECIDE WHICH EVENTS SHOULD BE WATCHED
$ErrorEvent = Register-ObjectEvent $watcher "Error" -Action $erroraction
$CreatedEvent = Register-ObjectEvent $watcher "Created" -Action $action
$ListOfEvents = #(
$ErrorEvent
$CreatedEvent
)
while ($true) {
$eventMissing = $false
$ListOfEvents | % {
$e = $_
if (!(Get-Event -SourceIdentifier $e.Name -ErrorAction SilentlyContinue)) {
# Event does not exist
$eventMissing = $true
}
}
if(!$watcher || $eventMissing -eq $true) {
# deregister events
$ListOfEvents | % {
$e = $_
try {
Unregister-Event -SourceIdentifier $e.Name
} catch {
# Do Nothing
}
}
if($watcher) {
$watcher.Dispose()
$watcher = $null
} else {
# Create watcher
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = $watcherArgs.Path
$watcher.Filter = $watcherArgs.Filter
$watcher.IncludeSubdirectories = $watcherArgs.IncludeSubdirectories
$watcher.EnableRaisingEvents = $watcherArgs.EnableRaisingEvents
$ErrorEvent = Register-ObjectEvent $watcher "Error" -Action $erroraction
$CreatedEvent = Register-ObjectEvent $watcher "Created" -Action $action
$ListOfEvents = #(
$ErrorEvent
$CreatedEvent
)
}
}
if ($watcher.EnableRaisingEvents -eq $false) {
$watcher.EnableRaisingEvents = $watcherArgs.EnableRaisingEvents
}
sleep 5
}
} finally {
$ListOfEvents | % {
$e = $_
try {
Unregister-Event -SourceIdentifier $e.Name
} catch {
# Do Nothing
}
}
if($watcher) {
$watcher.Dispose();
}
}
I am very new to powershell. The following code is created by BigTeddy and he gets the full credit (I also made some Changes using the While Loop)
I want to know how can I create an if/else statement such that if more than one file is changed/edited/created/deleted at the same time (say ten files have been edited simultaneously) a log file will be created saying these list files has been edited simultaneously at this particular time.
The following powershell script BigTeddy created basically spits out a log file (and on the output of the powershell ISE) of when a change/edit/creation/deletion has been made, the time it was changed and what file(s) was edited.
param(
[string]$folderToWatch = "C:\Users\gordon\Desktop\powershellStart"
, [string]$filter = "*.*"
, [string]$logFile = 'C:\Users\gordon\Desktop\powershellDest\filewatcher.log'
)
# In the following line, you can change 'IncludeSubdirectories to $true if required.
$fsw = New-Object IO.FileSystemWatcher $folderToWatch, $filter -Property #{IncludeSubdirectories = $false;NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'}
$timeStamp #My changes
$timeStampPrev = $timeStamp #My changes
# This script block is used/called by all 3 events and:
# appends the event to a log file, as well as reporting the event back to the console
$scriptBlock = {
# REPLACE THIS SECTION WITH YOUR PROCESSING CODE
$logFile = $event.MessageData # message data is how we pass in an argument to the event script block
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$timeStamp = $Event.TimeGenerated
while($timeStampPrev -eq $timeStamp) { #My changes
Write-Host "$timeStamp|$changeType|'$name'" -fore green
Out-File -FilePath $logFile -Append -InputObject "$timeStamp|$changeType|'$name'"
# REPLACE THIS SECTION WITH YOUR PROCESSING CODE
}
}
# Here, all three events are registered. You need only subscribe to events that you need:
Register-ObjectEvent $fsw Created -SourceIdentifier FileCreated -MessageData $logFile -Action $scriptBlock
Register-ObjectEvent $fsw Deleted -SourceIdentifier FileDeleted -MessageData $logFile -Action $scriptBlock
Register-ObjectEvent $fsw Changed -SourceIdentifier FileChanged -MessageData $logFile -Action $scriptBlock
# To stop the monitoring, run the following commands:
# Unregister-Event FileDeleted ; Unregister-Event FileCreated ; Unregister-Event FileChanged
#This script uses the .NET FileSystemWatcher class to monitor file events in folder(s).
#The advantage of this method over using WMI eventing is that this can monitor sub-folders.
#The -Action parameter can contain any valid Powershell commands.
#The script can be set to a wildcard filter, and IncludeSubdirectories can be changed to $true.
#You need not subscribe to all three types of event. All three are shown for example.
Have you considered a hash table with timestamps and action(create/modify/delete) as keys and file name as value. You iterate through the dictionary after a specific wait interval and you flush the entries in the dictionary to a log file.
I'm using a FileSystemWatcher to check for changed files in a target directory. However it does not seem like I can access the information on what file triggered the event, or I do simply not know how.
$Action = {
# Output name of trigger file here.
}
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher $TargetDirectory
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Changed -Action $Action
As I'm waiting for events in multiple target directories, the alternative of using synchronized waiting is no option for me.
Am I doing something wrong?
When using this code:
$Action = {
# Output name of trigger file here.
Write-Host $Event.SourceEventArgs.FullPath
}
$TargetDirectory = "c:\temp\fsw"
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher $TargetDirectory
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Changed -Action $Action
I get the full path of the file being changed.
I have my PowerShell project broken into modules. But because they are modules I have to reload them every time I change them. So I wrote a loop that has a FileSystemWatcher and if one of the .psm1 file changes it will either reload or import that module.
The problem is that the above loop isn't going to let me run other scripts in its working environment, so a new environment will not have the same modules loaded/reloaded for it. I need to keep these modules out of the primary default PowerShell modules folder(s). Is there a way to run the script that reloads the modules when they change in the same environment or affect a certain environment?
UPDATE
So I run the following Module-Loader.ps1 script. The code block associated with the 'FileChanged' event does fire when I 'save' a *.psm1 file after having been modified. However two issues occure:
1) it fires twice when I save
2a) If the module is not loaded, it will run Import-Module $PWD\ModuleName, but it won't have actually loaded at least in the environment (if I run the same code in the environment it will load)
2b) if it is loaded, and it tries to remove the module, it will error that none exists.
# create a FileSystemWatcher on the currect directory
$filter = '*.psm1'
$folder = $PWD
$watcher = New-object IO.FileSystemWatcher $folder, $filter -Property #{IncludeSubdirectories = $false; EnableRaisingEvents = $true; NotifyFilter = [IO.NotifyFilters]'LastWrite'}
Register-ObjectEvent $watcher Changed -SourceIdentifier FileChanged -Action {
$name = $Event.SourceEventArgs.Name
$filename = $name.Remove($name.IndexOf('.'), 5)
$loadedModule = Get-Module | ? { $_.Name -eq $filename }
write-host $filename
if ($loadedModule) {
write-host "Reloading Module $folder\$($filename)"
Reload-Module $folder\$filename
} else {
write-host "Importing Module $folder\$($filename)"
Import-Module $folder\$filename
}
}
I am of the opinion that though this is being ran in a session, the code block in the event is not associated with this specific environment.
Here is an example from some code I have that copies a folder to a shared folder any time something has changed in it. It's kinda my little dropbox implementation :-)
Any time one of the file system watcher event types such as Changed occurs, the code specified in the -Action parameter of the Register-ObjectEvent cmdlet will fire.
In your -Action code you would call Import-Module with the -Force parameter to overwrite the current one in
memory.
function Backup-Folder {
& robocopy.exe "c:\folder" "\\server\share" /MIR /W:10 /R:10
}
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "c:\folder"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
Register-ObjectEvent $watcher "Changed" -Action { Backup-Folder }
Register-ObjectEvent $watcher "Created" -Action { Backup-Folder }
Register-ObjectEvent $watcher "Deleted" -Action { Backup-Folder }
Register-ObjectEvent $watcher "Renamed" -Action { Backup-Folder }