Monitoring files in Powershell and using ArrayList - powershell

I'm new to PowerShell. I would like to add a file path to an ArrayList every time it changes. However, this PowerShell script fails somehow. Any hints what might I be doing wrong?
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\Mydir"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
$jobs = New-Object System.Collections.ArrayList
$changed = Register-ObjectEvent $watcher "Changed" -Action {
$changedFile = $($eventArgs.FullPath)
$jobs.Add($changedFile)
}

It's a scope issue. Add the global scope modifier:
$global:jobs.Add($changedFile)
see about_Scopes for more help.

Related

Powershell FileSystemWatcher - Avoiding duplicate action on Create

It's a known issue in Powershell that the FileSystemWatcher fires twice on events. I am trying to work around this as I am watching for files being created and then pushing it to a printer. The double firing means I am getting duplicated printouts
I know this question has been asked before but I am a complete newb when it comes to Powershell (and scripting in general really) so some of the answers have gone straight over my head
In the code, I am watching a folder and then passing the subdirectory names as the printer name for sending the job. This is because the software in use is copying the pdf files from a remote location into those folders (the software doesn't have direct access to the printers due to citrix)
### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "L:\Label\"
$watcher.Filter = "*.*"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
$action = { $path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$printer = Split-Path (Split-Path $path -Parent) -Leaf
$logline = "$(Get-Date), $changeType, $path, $printer"
Add-content "c:\prog\log.txt" -value $logline
C:\prog\SumatraPDF.exe -print-to "\\http://srv:631\$printer" $path
}
### DECIDE WHICH EVENTS SHOULD BE WATCHED
Register-ObjectEvent $watcher "Created" -Action $action
while ($true) {sleep 5}
I expect to see the printing command (the Sumatra call) only occur once when a pdf file is dropped into the watch folder
Instead of telling you what you should or shouldn't do, here is how to do what you asked for:
### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$global:canDoEvent = $True #NEW
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "L:\Label\"
$watcher.Filter = "*.*"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
DEFINE ACTIONS AFTER AN EVENT IS DETECTED
$action = { if ($global:canDoEvent) { #NEW
$global:canDoEvent = $False #NEW
$path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$printer = Split-Path (Split-Path $path -Parent) -Leaf
$logline = "$(Get-Date), $changeType, $path, $printer"
Add-content "c:\prog\log.txt" -value $logline
C:\prog\SumatraPDF.exe -print-to "\\http://srv:631\$printer" $path
}
}
DECIDE WHICH EVENTS SHOULD BE WATCHED
Register-ObjectEvent $watcher "Created" -EventName newFile -Action $action #NEW
do { #NEW
$global:canDoEvent = $True
Wait-Event -SourceIdentifier newFile -Timeout 1
} while ($True)
It probably requires tuning, I'm no expert, but that's the idea.
Basically, add a global boolean var = True, put your wait-event on a timeout inside a do-while loop, make variable true every loop, then in your event action, make it false. your timeout will define how often the event can fire. a single second should suffice to prevent multiple firing events. Obviously, if there are contexts where more than 1 unique file could be created and printed during the same second, it would skip them.
I don't think it's a known issue that the FileSystemWatcher fires twice on events, not where you got that information.
Regardless, if I were you I wouldn't code the FileSystemWather events in Powershell myself, it's a real pain.
Instead, you can use Powershell Guard, just pass the print command instead of the TestCommand. https://github.com/smurawski/PowerShellGuard
All that PowerGuard does is abstract the use of the FileSystemWatcher. You can create your print command in a Powershell script, and just have PowerGuard call your script with -TestCommand "Print.ps1 .\PathToYourFile"
Final Solution (by the poster himself):
dir \\srv\label\prnlblCuts\*.pdf | New-Guard -TestCommand "C:\PROG\SumatraPDF.exe -print-to \\srv-tsv:631\prnlblCuts" -TestPath "$($_.FullName)" -Wait

PowerShell File Monitoring and Text-to-Speech

I am new to PowerShell and I'm trying to help a friend write a script that will constantly monitor a file and whenever the file changes, the new text in the file is read aloud (the text file is constantly edited and all of the old content within it is replaced by new content which is taken from emails as they arrive).
The script works perfectly as far as pulling the content from the file and reading it aloud, but I'm having one small issue in that it reads the contents either two or four times, while I only need it to read it once.
Additionally, while PowerShell is speaking the content, it doesn't update/queue changes in the file, so if two changes are made while PowerShell is speaking a prior change, the first change is skipped over and only the most recent change is read aloud. Is there a way to make it queue all changes of the file and read them sequentially?
What I currently have is
Add-Type -AssemblyName System.speech
$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer
$speak.Rate = 0 # -10 is slowest, 10 is fastest
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\Users\Dylan\Desktop\"
$watcher.Filter = "alarm.txt"
$watcher.IncludeSubdirectories = $false
$watcher.EnableRaisingEvents = $true
$AlarmLocation = "C:\Users\Dylan\Desktop\alarm.txt"
$changeAction = {
$Alarm = (Get-Content $AlarmLocation)
$speak.Speak($Alarm)
}
Register-ObjectEvent $watcher "Changed" -Action $changeAction
while ($true) {sleep 5}
Am I missing something obvious here or is there a different function I have to include?
Thank you
The issue with multiple events is explained here:
https://blogs.msdn.microsoft.com/oldnewthing/20140507-00/?p=1053/
One way to deal with it is to keep track of LastWriteTime.
We can run the speaker in a different thread, so it doesn't block the watcher. That way we will detect if the file changes while speaking.
Something like this...
# When the file is changed,
# the content is stored in the queue,
# and the speaker is signaled.
# it breaks, if the file changes very rapidly.
# use a hashtable for all the vars
# for easier transport across scopes
$vars = [hashtable]::Synchronized(#{})
$vars.speakQueue = New-Object System.Collections.Queue
$vars.speakEvent = New-Object System.Threading.AutoResetEvent $false
$vars.speakLastWriteTime = [DateTime]::MinValue
$vars.speakRunning = $true
$vars.speakPS = [System.Management.Automation.PowerShell]::Create().AddScript({
# this is the speaker thread
Param (
$vars
)
Add-Type -AssemblyName System.speech
$speak = New-Object System.Speech.Synthesis.SpeechSynthesizer
$speak.Rate = 0 # -10 is slowest, 10 is fastest
# run until other thread sets running=false
while($vars.speakRunning) {
# other thread sets the event when content is available
if($vars.speakEvent.WaitOne(100)) {
# use System.Threading.Monitor to make queue thread safe
[System.Threading.Monitor]::Enter($vars.SyncRoot)
try {
# get all alarms
$alarm = while($vars.speakQueue.Count){ $vars.speakQueue.Dequeue() }
}
catch {
}
[System.Threading.Monitor]::Exit($vars.SyncRoot)
# speak now
$alarm | ForEach-Object { $speak.Speak($_) }
}
}
}).AddArgument($vars)
# start new thread
$vars.speakPSHandle = $vars.speakPS.BeginInvoke()
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\Users\Dylan\Desktop\"
$watcher.Filter = "alarm.txt"
$watcher.IncludeSubdirectories = $false
$watcher.EnableRaisingEvents = $true
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite
$changeAction = {
$vars = $event.MessageData
# FullPath is the path of the changed file
$item = Get-Item $event.SourceEventArgs.FullPath
# only proceed, if LastWriteTime has changed
if($item.LastWriteTime -ne $vars.speakLastWriteTime) {
$vars.speakLastWriteTime = $item.LastWriteTime
$alarm = Get-Content $event.SourceEventArgs.FullPath -Raw
[System.Threading.Monitor]::Enter($vars.SyncRoot)
try {
# put content in queue
$vars.speakQueue.Enqueue($alarm)
}
catch {
}
[System.Threading.Monitor]::Exit($vars.SyncRoot)
# signal speaker in other thread
$vars.speakEvent.Set()
}
}
$job = Register-ObjectEvent $watcher "changed" -Action $changeAction -SourceIdentifier "FileChanged" -MessageData $vars
while($true) {
Start-Sleep -Milliseconds 25
}
# clean-up, if ever needed...
Unregister-Event "FileChanged"
$vars.speakRunning = $false # leaves while-loop in thread
$vars.speakPS.EndInvoke($vars.speakPSHandle) # waits for thread to end

How to find the file that triggered a FileSystemWatcher event

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.

Powershell get-members of $EventArgs automatic variable

I want to list all available properties of the variable $EventArgs by piping it to get-member. but I am having trouble to get the members of the automatic variable $EventArgs.
In the example I can see that they get the property. FullPath from the automatic variable $EventArgs. I want a way to list all the properties that maybe are useful.
Any ideas of how to get the members of $EventArgs automatic variable.
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "C:\"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
$watcher.Site
$arrary = #()
$watcher.Filter = ""
Register-ObjectEvent $watcher "Created" -Action {
write-host "Created: $($eventArgs.FullPath)"
$arrary+= $EventArgs|gm
$arrary+= $EventArgs
write-host $EventArgs|gm
$EventArgs|gm
}
One quick way to see all of the properties (there are only three) is to do this:
Register-ObjectEvent $watcher "Created" -Action {
$eventArgs | Select-Object * | Write-Host
}
However, you'll get much more useful information if you do this:
Register-ObjectEvent $watcher "Created" -Action {
Write-Host $eventArgs.GetType()
}
and then look up the resultant type on MSDN. When you do that, you'll see that $EventArgs is actually an object of type System.IO.FileSystemEventArgs, which is fully documented here. Not only will you see the three properties, you'll see what they mean. For example, the first property ChangeType is really an enumeration of the type System.IO.WatcherFileTypes, and you can learn all of the different values it can have.

How can I reload modules from one open work environment to affect another working environment

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 }