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();
}
}
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
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
I have a text file that is written to by a CNC Controller, the file is locked until a certain command in executed on the CNC Controller, I want to have a PowerShell Script running that keeps checking this file and once it is no longer locked, PowerSheel opens up a certain Webpage on our Intranet that processes the file and stores that data in an SQL database.
I've seen some scripts to check if a file is locked and seen some that constantly monitors a file for if its Changed, i need a mix of both
P.S. i'm a complete novice on PowerShell!!!
This is the Script I want to use but want it not to look for CHANGED events but once the file $OUT$.DAT is no longer locked by nother process
### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "c:\ftpserver"
$watcher.Filter = "*.dat"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
### DEFINE ACTIONS AFTER AN EVENT IS DETECTED
$action = { $path = $Event.SourceEventArgs.FullPath
$changeType = $Event.SourceEventArgs.ChangeType
$logline = "$(Get-Date), $changeType, $path"
Add-content "c:\ftpserver\log.txt" -value $logline
}
### DECIDE WHICH EVENTS SHOULD BE WATCHED
Register-ObjectEvent $watcher "Changed" -Action $action
while ($true) {sleep 5}
You can modify that code and add it to a function i.e. :
function checkLock {
Param(
[parameter(Mandatory=$true)]
$filename
)
$file = gi (Resolve-Path $filename) -Force
if ($file -is [IO.FileInfo]) {
trap {
return $true
continue
}
$stream = New-Object system.IO.StreamReader $file
if ($stream) {$stream.Close()}
}
return $false
}
You can then just chuck the function call into an if statement or a loop like so:
if ((checkLock $filename) -eq $true) {
Write-Host "file locked"
continue
}
else {
Write-Host "file not locked"
<< code you want to run on unlocked file >>
}
Timed loop:
While ($True) {
if ((checkLock $filename) -eq $true) {
Write-Host "file locked"
continue
}
else {
Write-Host "file not locked"
<< code you want to run on unlocked file >>
}
start-sleep -seconds 10
}
This will run continually until canceled. If you want it to run for a set amount of time use:
$timeout = new-timespan -Minutes 1
$sw = [diagnostics.stopwatch]::StartNew()
While ($sw.elapsed -lt $timeout) {
if ((checkLock $filename) -eq $true) {
Write-Host "file locked"
continue
}
else {
Write-Host "file not locked"
<< code you want to run on unlocked file >>
}
start-sleep -seconds 10
}
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.
I've been scratching my head on this one for several hours ... The code below triggers an action whether a notification ballon is clicked on or closed. Also I'm new to PowerShell.
Consider this code :
####### Launch as : ##########################
## powershell.exe -sta -file .\balloon.ps1 ##
##############################################
Write-Host -ForeGround Yellow " ###### START OF SCRIPT ! ######"
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Title = "This is the title"
$Text = "This is the text"
$EventTimeOut = 5
$balloon = New-Object System.Windows.Forms.NotifyIcon
$balloon.Icon = [System.Drawing.SystemIcons]::Information
$balloon.BalloonTipTitle = $Title
$balloon.BalloonTipText = $Text
$balloon.Visible = $True
$balloon.ShowBalloonTip(1)
Register-ObjectEvent $balloon BalloonTipClicked -SourceIdentifier event_BalloonTipClicked `
-Action {
# explorer.exe; `
Write-Host -ForeGround Green "event_BalloonTipClicked occured !"; `
# Gets rid of icon
$balloon.Dispose(); `
}|Out-Null
Register-ObjectEvent $balloon BalloonTipClosed -SourceIdentifier event_BalloonTipClosed `
-Action {
Write-Host -ForeGround Green "event_BalloonTipClosed occured !"; `
$balloon.Dispose(); `
}|Out-Null
Wait-Event event_BalloonTipClicked -TimeOut $EventTimeOut
Wait-Event event_BalloonTipClosed -TimeOut $EventTimeOut
Unregister-Event -SourceIdentifier event_BalloonTipClicked
Unregister-Event -SourceIdentifier event_BalloonTipClosed
Write-Host -ForeGround Gray "Should be empty -- start --"
Get-EventSubscriber
Write-Host -ForeGround Gray "Should be empty -- end --"
#[System.Windows.Forms.MessageBox]::Show("DoneĀ !!")
Write-Host -ForeGround Yellow " ###### END OF SCRIPT ! ######"
I would like the script to end as soon as "Register-ObjectEvent" finished triggering its actions.
However return only happens after the timeout specified in "Wait-Event", preventing further execution of code. This behavior also prevents me from turning that code into a function.
The script behaves the same way if it listens to one event only.
Any help would be much appreciated !
Tanks for your time Shay Levy. I haven't managed to use your solution properly.
I used this vbscript to launch the file in the background.
Set objShell = CreateObject("Wscript.Shell")
objShell.Run "powershell.exe -NoExit -Sta -File .\balloon.ps1",0
Unfortunately, launching the script 5 times for instance would leave me 5 running powershell instances doing nothing.
I finally realised that mixing "Register-ObjectEvent -Action" and "Wait-Event" is a definite no-no (http://social.technet.microsoft.com/Forums/en-US/winserverpowershell/thread/10983ec3-7aa6-4011-a87e-a30a25ab484a/)
The following code is what I was aiming. It is a synchronous approach to the problem.
###################################################
## Launch as : ##
## cmd /k powershell -Sta [-File] .\balloon.ps1 ##
###################################################
# This post put me on the right track "http://social.technet.microsoft.com/Forums/en-US/winserverpowershell/thread/10983ec3-7aa6-4011-a87e-a30a25ab484a/"
Write-Host -ForeGround Yellow " ###### START OF SCRIPT ! ######"
[void][System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Title = "This is the title"
$Text = "This is the text"
$EventTimeOut = 5
$balloon = New-Object System.Windows.Forms.NotifyIcon -Property #{
Icon = [System.Drawing.SystemIcons]::Information
BalloonTipTitle = $Title
BalloonTipText = $Text
Visible = $True
}
# Value "1" here is meaningless. $EventTimeOut will force bubble to close.
$balloon.ShowBalloonTip(1)
Register-ObjectEvent $balloon BalloonTipClicked -SourceIdentifier event_BalloonTipClicked
Register-ObjectEvent $balloon BalloonTipClosed -SourceIdentifier event_BalloonTipClosed
# "Wait-Event" pauses the script here until an event_BalloonTip* is triggered
# TimeOut is necessary or balloon and script hangs there forever.
# This could be okay but event subscription gets messed up by following script instances generating the same event names1.
$retEvent = Wait-Event event_BalloonTip* -TimeOut $EventTimeOut
# Script resumes here.
$retSourceIdentifier = $retEvent.SourceIdentifier
If ($retSourceIdentifier -eq $null){
Write-Host -ForeGround Green "TimeOut occured !"
}Else{
Write-Host -ForeGround Green "$retSourceIdentifier occured !"
}
If ($retSourceIdentifier -eq "event_BalloonTipClicked"){
explorer.exe
}
# Gets rid of icon. This is absolutely necessary, otherwise icon is stuck event if parent script/shell closes
$balloon.Dispose()
# Tidy up, This is needed if returning to parent shell.
Unregister-Event -SourceIdentifier event_BalloonTip*
Get-Event event_BalloonTip* | Remove-Event
Write-Host -ForeGround Gray "Should be empty -- start --"
Get-EventSubscriber
Write-Host -ForeGround Gray "Should be empty -- end --"
#[System.Windows.Forms.MessageBox]::Show("Done !!")
Write-Host -ForeGround Yellow " ###### END OF SCRIPT ! ######"
The script registers two event handlers. Once an event was triggered it
executes the code in its action parameter and un-register the event handler and the job it produces.
If un-register the events is not what you want you can comment out the relevant lines (see inline comment).
When you run powershell from a batch file you may want to add the -NoExit switch so the console will not close (and destroy any events and related jobs).
####### Launch as : ##########################
## powershell.exe -sta -file .\balloon.ps1 ##
##############################################
Write-Host -ForeGround Yellow " ###### START OF SCRIPT ! ######"
Add-Type -Assembly System.Windows.Forms
$Title = "This is the title"
$Text = "This is the text"
$balloon = New-Object System.Windows.Forms.NotifyIcon -Property #{
Icon = [System.Drawing.SystemIcons]::Information
BalloonTipTitle = $Title
BalloonTipText = $Text
Visible = $True
}
$balloon.ShowBalloonTip(1)
$null = Register-ObjectEvent $balloon BalloonTipClicked -SourceIdentifier event_BalloonTipClicked -Action {
Write-Host -ForeGround Green "event_BalloonTipClicked occured !"
Unregister-Event -SourceIdentifier $event.SourceIdentifier -Force
Remove-Job $event.SourceIdentifier -Force
# unregister event and remove job object
Unregister-Event -SourceIdentifier event_BalloonTipClosed -Force
Remove-Job event_BalloonTipClosed -Force
$balloon.Dispose()
}
$null = Register-ObjectEvent $balloon BalloonTipClosed -SourceIdentifier event_BalloonTipClosed -Action {
Write-Host -ForeGround Green "event_BalloonTipClosed occured !"
Unregister-Event -SourceIdentifier $event.SourceIdentifier -Force
Remove-Job $event.SourceIdentifier -Force
# unregister event and remove job object
Unregister-Event -SourceIdentifier event_BalloonTipClicked -Force
Remove-Job event_BalloonTipClicked -Force
$balloon.Dispose()
}