Filesystemwatcher file created event not decompressing files - powershell

I got a simple script that should watch for new files in specified folder and uncompress them on arriving. Simple, but my script only works under the debugger of Powershell ISE. When outside the debugger, on file creation it only prints "New file xxx found" and "Done", without decompressing it, nor deleting, but also no errors. I'm I missing something here?
# Folder you to monitor
$PathToMonitor = "C:\tmp1".
$OutputPath = "C:\tmp"
$Filter = "*.zip" # Filter for zip files.
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher
$FileSystemWatcher.Path = $PathToMonitor
$FileSystemWatcher.IncludeSubdirectories = $false
$FileSystemWatcher.Filter = $Filter
# make sure the watcher emits events
$FileSystemWatcher.EnableRaisingEvents = $true
# define the code that should execute when a file change is detected
$Action = {
$details = $event.SourceEventArgs
$FullPath = $details.FullPath
$OldName = $details.OldName
$Timestamp = $event.TimeGenerated
try
{
Write-Host "New file " + $FullPath + " found"
$TarProgram = "tar.exe"
$TarArgs = 'xvf "' + $FullPath + '" -C ' + $OutputPath
Start-Process $TarProgram $TarArgs -Wait -WindowStyle hidden
Remove-Item -Path $FullPath -Force
Write-Host "Done"
}
catch [System.SystemException]
{
Write-Host $_.ScriptStackTrace
}
}
# add event handlers
$handlers = . {
Register-ObjectEvent -InputObject $FileSystemWatcher -EventName Created -Action $Action -SourceIdentifier FSCreateFtpGe
}
Write-Host "Watching for changes to $PathToMonitor"
try
{
do
{
Wait-Event -Timeout 10
} while ($true)
}
catch [System.SystemException]
{
Write-Host $_.ScriptStackTrace
}
finally
{
# this gets executed when user presses CTRL+C
# remove the event handlers
Write-Host "Cleaning up"
Unregister-Event -SourceIdentifier FSCreateFtpGe
# remove background jobs
$handlers | Remove-Job
# remove filesystemwatcher
$FileSystemWatcher.EnableRaisingEvents = $false
$FileSystemWatcher.Dispose()
"Event Handler disabled."
}

Why do you have a period here...
$PathToMonitor = "C:\tmp1".
...that is invalid syntax and the script should fail anyway. Yet, that could be just a type you did on posting here.
Why are you using tar vs the PowerShell built-in archive cmdlets?
if tar.exe is not in your PowerShell or system paths, you have to fully qualify that path to it for it to be used.
$TarProgram = 'C:\ProgramFiles\tar.exe'
So, your code should work in any PowerShell host (ISE [in/out of debug mode])/powershell.exe/VSCode, et al. AS I just tested it. Yet again, the cmdlets are there. Example of the below running in the new Windows Terminal.
#region Begin Script
# Folder you to monitor
$PathToMonitor = 'D:\tmp1'
$OutputPath = 'D:\tmp'
$Filter = '*.zip' # Filter for zip files.
$FileSystemWatcher = New-Object System.IO.FileSystemWatcher
$FileSystemWatcher.Path = $PathToMonitor
$FileSystemWatcher.IncludeSubdirectories = $false
$FileSystemWatcher.Filter = $Filter
# make sure the watcher emits events
$FileSystemWatcher.EnableRaisingEvents = $true
# define the code that should execute when a file change is detected
$Action = {
$details = $event.SourceEventArgs
$FullPath = $details.FullPath
$OldName = $details.OldName
$Timestamp = $event.TimeGenerated
try
{
"New file $FullPath found"
Expand-Archive -Path $FullPath -DestinationPath 'D:\tmp'
# Start-Process $TarProgram $TarArgs -Wait -WindowStyle hidden
Remove-Item -Path $FullPath -Force
'Done'
}
catch [System.SystemException]
{$PSItem.ScriptStackTrace}
}
# add event handlers
$handlers = . {
$RegisterObjectEventSplat = #{
InputObject = $FileSystemWatcher
EventName = 'Created'
Action = $Action
SourceIdentifier = 'FSCreateFtpGe'
}
Register-ObjectEvent #RegisterObjectEventSplat
}
"Watching for changes to $PathToMonitor"
try
{
do { Wait-Event -Timeout 10 }
while ($true)
}
catch [System.SystemException]
{$_.ScriptStackTrace}
finally
{
<#
this gets executed when user presses CTRL+C
remove the event handlers
#>
'Cleaning up'
Unregister-Event -SourceIdentifier FSCreateFtpGe
# remove background jobs
$handlers |
Remove-Job
# remove filesystemwatcher
$FileSystemWatcher.EnableRaisingEvents = $false
$FileSystemWatcher.Dispose()
'Event Handler disabled.'
}
#endregion End Script
# Results
Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -Property Caption, Version
Caption Version
$PSVersionTable
Name Value
---- -----
PSVersion 5.1.18362.752
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0, 5.0, 5.1.18362.752}
BuildVersion 10.0.18362.752
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
Get-ChildItem -Path 'D:\tmp' -Verbose
(Get-ChildItem -Path 'D:\tmp').Count
0
Get-ChildItem -Path 'D:\tmp1' -Verbose
(Get-ChildItem -Path 'D:\tmp1').Count
0
.\FSWCopyUnzip.ps1
Watching for changes to D:\tmp1
New file D:\tmp1\book1.zip found
Done
Cleaning up
Get-ChildItem -Path 'D:\tmp' -Verbose
Directory: D:\tmp
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 13-Mar-20 12:19 70 abc.txt
-a---- 13-Mar-20 22:27 71 book1.txt
-a---- 06-Apr-20 21:51 2979 Data.zip
-a---- 04-Mar-20 01:04 72 FileWithLines.txt
Get-ChildItem -Path 'D:\tmp1' -Verbose
(Get-ChildItem -Path 'D:\tmp1').Count
0

Just for the records, I found the issue. It's somehow related to variables scopes in Powershell.
If instead of using a variable declared at the begin of the script to hold the value of $OutputPath I use a literal string then it works. Otherwise, it complains that the value is null at runtime (In Powershell ISE it shows a value)
So, the action block now looks like this:
$Action = {
$details = $event.SourceEventArgs
$FullPath = $details.FullPath
$OldName = $details.OldName
$Timestamp = $event.TimeGenerated
try
{
Expand-Archive -LiteralPath $FullPath -DestinationPath "C:\Temp"
Remove-Item -LiteralPath "$FullPath" -Force
}
catch [System.SystemException]
{
Write-Host $_
}
}
I would prefer to have all my vars at the begin, so they can be easily changed if needed, but at least this version works.

Related

Powershell Scipt Runs in ISE only

I have researched this question quite a bit and have found others with similar issues, but no fix that works for us.
We have built a power shell script to monitor a folder for new images, check their color profile with exiftool and convert when needed with image magick. All of this functionality works well enough when the script is run in ISE, but will not work if the script is executed from a .exe or ran manually.
I have read that FileSystemWatcher requires the script be ran in STA which I have attempted to do a few times by including -STA in the target for the .exe shortcut. Doing so proves unsuccessful.
#----------------------------------------------------------------------------
Unregister-Event -SourceIdentifier FileCreated
Clear
#----------------------------------------------------------------------------
$description = Get-WmiObject -Class Win32_OperatingSystem |Select Description
$description = $description -Replace "#{Description=",""
$description = $description -Replace "}",""
$assetsFolder = "E:\" + $description + "\Capture One Session\Output"
$conversionStage = "C:\Profile_Pro\conversionStage"
#----------------------------------------------------------------------------
New-Item -ItemType directory -Force -Path "$assetsFolder" | Out-Null
New-Item -ItemType directory -Force -Path "$assetsFolder" | Out-Null
#----------------------------------------------------------------------------
$filter = "*.*"
$srgb = "sRGB IEC61966-2.1"
$isTif = "tif"
#----------------------------------------------------------------------------
$monitor = New-Object IO.FileSystemWatcher $assetsFolder, $filter -Property #{
IncludeSubdirectories = $true
NotifyFilter = [IO.NotifyFilters]'FileName, LastWrite'
}
#----------------------------------------------------------------------------
$onCreated = Register-ObjectEvent $monitor Created -SourceIdentifier FileCreated -Action {
#----------------------------------------------------------------------------
Add-Type -AssemblyName System.Windows.Forms
#----------------------------------------------------------------------------
$path = $event.SourceEventArgs.FullPath
$name = $event.SourceEventArgs.Name
Write-Host $path
Write-Host $name
If ($name -Match $isTif)
{
$BOX = [System.Windows.Forms.MessageBox]::Show('TIFF FILE DETECTED. PLEASE CHECK RECIPE.', 'Warning', 'ok', 'Warning')
$path = $path -Replace ".tif.tmp",".tif"
Start-Sleep -s 2
Remove-Item "$path" -Force
}
Else
{
$colorSpace = C:\Profile_Pro\tools\exiftool.exe -T -ProfileDescription $path
$profileConvert = 'C:\Profile_Pro\tools\sRGB_Color_Space_Profile.icm'
If ($colorSpace -Match $srgb)
{
}
Else
{
Start-Sleep -s 3
$name = $name -Replace ".jpg.tmp",".jpg"
$path = $path -Replace ".jpg.tmp",".jpg"
Move-Item -Path $path -Destination $conversionStage -Force
convert $conversionStage\$name -profile $profileConvert $path
Start-Sleep -s 1
Remove-Item "$conversionStage\$name" -Force
#----------------------------------------------------------------------------
$BOX = [System.Windows.Forms.MessageBox]::Show('COLOR PROFILE ERROR. PLEASE CHECK RECIPE.', 'Warning', 'ok', 'Warning')
#----------------------------------------------------------------------------
$TO = $env:USERNAME + "#biz.com"
$Outlook = New-Object -ComObject Outlook.Application
$Mail = $Outlook.CreateItem(0)
$Mail.To = $TO
$Mail.Subject = "COLOR PROFILE DEFECT: " + $name
$Mail.importance = 2
$Mail.Body = "A COLOR PROFILE DEFECT HAS BEEN DETECTED WITH FILE:`r`n`r`n$name`r`n`r`nTHIS FILE IS FIXED.`r`n`r`nPLEASE CHECK YOUR PROCESS RECIPE."
$Mail.Send()
}
}
}
#----------------------------------------------------------------------------
#Unregister-Event -SourceIdentifier FileCreated
#----------------------------------------------------------------------------
Any ideas to keep this script running as a .exe or outside of ISE would be greatly appreciated.

How to send email for multiple files uploaded in one email

I am using a powershell script that monitors a folder for new files added. This script works great the only problem is that it sends an email for every new file added. I can't seem how to get it to send a single email for all files added if a user was to drag and drop multiple files to folder. For instance if a user drops 3 files in the folder it should send one email with the links to all three files. This is my first time using powershell scripts.
PowerShell.exe -windowstyle hidden{
$MonitorFolder = "C:\Users\RickG\Desktop\Test Reports"
$MonitorStopFile = "monitor.die"
$smtpServer = "mail.rtg.org"
$smtpFrom = "SYSTEMFUNCTION#rtg.org"
$smtpTo = "rickg#rtg.org"
$smtpSubject = "New file arrived in $($MonitorFolder)."
$SourceID = "MonitorFiles"
$Query = #"
SELECT * FROM __InstanceCreationEvent WITHIN 10
WHERE targetInstance ISA 'Cim_DirectoryContainsFile'
AND targetInstance.GroupComponent = 'Win32_Directory.Name="$($MonitorFolder.Replace("\", "\\\\"))"'
"#
Try {
$smtp = New-Object -TypeName "Net.Mail.SmtpClient" -ArgumentList $smtpServer
Register-WmiEvent -Query $Query -SourceIdentifier $SourceID
Do {
"Waiting for a new file to arrive in '$($MonitorFolder)'; to stop, hit <Ctrl-C> or create a file '$MonitorStopFile'." | Write-Host
$FileEvent = Wait-Event -SourceIdentifier $SourceID
Remove-Event -EventIdentifier $FileEvent.EventIdentifier
$FileName = $FileEvent.SourceEventArgs.NewEvent.TargetInstance.PartComponent.Split("=", 2)[1].Trim('"').Replace("\\", "\")
If ((Split-Path -Path $FileName -Leaf) -eq $MonitorStopFile) {
$smtpBody = "[$(Get-Date -Format HH:mm:ss)]`tStop file arrived: '$($FileName)'; monitor is going down!"
Remove-Item -Path (Join-Path -Path $MonitorFolder -ChildPath $MonitorStopFile)
$FileEvent = $Null
} Else {
$smtpBody = "[$(Get-Date -Format HH:mm:ss)]`tNew file arrived: $($FileName)"
}
$smtpBody | Write-Host -Fore Yellow
$smtp.Send($smtpFrom, $smtpTo, $smtpSubject, $smtpBody)
} While ($FileEvent)
} Catch {
$_ | Out-String | Write-Error
} Finally {
Remove-Event -SourceIdentifier $SourceID -ErrorAction SilentlyContinue
Unregister-Event -SourceIdentifier $SourceID -ErrorAction SilentlyContinue
}
}
Also I am trying to make the file a link in the body of the email. I have tried this line with no luck.
$smtpBody = "[$(Get-Date -Format HH:mm:ss)]`tNew file arrived: <a href='$($FileName)'>$($FileName)</a>"

Use of Start-job for WatcherChangeTypes

Is there a way to monitor the file changes in the directory in the powershell background.
I was trying to do as below.
Start-Job {
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = get-location
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $false
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor [System.IO.NotifyFilters]::FileName
while($true){
$result = $watcher.WaitForChanged([System.IO.WatcherChangeTypes]::Changed -bor [System.IO.WatcherChangeTypes]::Renamed -bOr [System.IO.WatcherChangeTypes]::Created, 1000);
if($result.TimedOut){
continue;
}
Add-Content D:\receiver.txt "file name is $($result.Name)"
}
}
This does not work as expected. I do not get any information on the receiver.txt file. Although the script works as expected if I dont use start-job.
Start-Job will launch your job in a new context, so the working directory will be the default directory (e.g. \Users\Username) and Get-Location will return this directory.
One way to deal with this is to save the original working directory, pass it to the job as an argument, and set the working directory in the job using Set-Location.
$currentLocation = Get-Location
Start-Job -ArgumentList $currentLocation {
Set-Location $args[0];
...
}
I would use Register-ObjectEvent and track each event type. It uses the same PSJobs that you would use in Start-Job, but geared towards monitoring the actual event and running a specific action based on what you give it.
Not tested:
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = get-location
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $false
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor [System.IO.NotifyFilters]::FileName
ForEach ($Item in #('Changed','Renamed','Created')) {
(Register-ObjectEvent -EventName $Item -InputObject $watcher -Action {
#Set up a named mutex so there are no errors accessing an opened file from another process
$mtx = New-Object System.Threading.Mutex($false, "FileWatcher")
$mtx.WaitOne()
Add-Content D:\receiver.txt "file name is $($result.Name)"
#Release so other processes can write to file
[void]$mtx.ReleaseMutex()
})
}
Quick way to stop the FileSystemWatcher
Get-EventSubscriber | Unregister-Event
Get-Job | Remove-Job -Force

FileSystemWatcher cmdlet in Powershell not monitoring system32 folder and subfolders

I noticed that when using PowerShell's FileSystemWatcher cmdlet that it doesnt seem to monitor System32 or it's subfolders in my Windows 7 computer. A script Í have that works just fine when monitoring subfolders of My Documents (i/e "C:\Users\W\Documents\Fichiers PowerShell" is a fplder path that works) but doesn't work when I substitute a folder path in System32 (i/e C:\Windows\System32\Recovery is a path that doesn't work)
Here is the script i'm working with but System32 paths haven't worked in other FileSystemWatcher scripts. Any advice as to a workaround would be appreciated . I must monitor C:\Windows\System32\Recovery. Thank You.
function FileSystemWatcher([string]$log = "C:\Users\W\Documents\Fichiers PowerShell\newfiles.txt",
[string]$folder = "C:\Windows\System32\Recovery",
[string]$filter = "*ps1",
[char]$timeout = 1000
){
$FileSystemWatcher = New-object System.IO.FileSystemWatcher $folder, $filter
Write-Host "Press any key to abort monitoring $folder"
do {
$result = $FileSystemWatcher.WaitForChanged("created", $timeout)
if ($result.TimedOut -eq $false) {
$result.Name |
Out-File $log -Append
Write-Warning ("Detected new file: " + $result.name)
$filename = (gc $log -ea 0)
ii "C:\Program Files (x86)\Mozilla Firefox\firefox.exe"
remove-item "C:\Users\W\Documents\Fichiers PowerShell\newfiles.txt"
}
} until ( [System.Console]::KeyAvailable )
Write-Host "Monitoring aborted."
Invoke-Item $log}
FileSystemWatcher
Give this a try:
$fsw = New-Object System.IO.FileSystemWatcher C:\Windows\System32 -Property #{
IncludeSubdirectories = $true
NotifyFilter = [System.IO.NotifyFilters]::DirectoryName
}
$event = Register-ObjectEvent $fsw Created -SourceIdentifier DirectoryCreated -Action {
Write-Host "$($event.SourceArgs | fl * | Out-String)"
}
md C:\Windows\System32\temp2

How do I tail a log file that's being written because of an earlier command in a PowerShell script?

I'm scripting the installation and configuration procedure for my company's desktop application. We send out a kiosk and can't put everything in the installer... moving right along! I'm using Start-Process to wait for msiexec to complete.
function Run-Installer
{
param
(
[string] $msi = $(throw "Required parameter: 'msi'"),
)
if(-not(Test-Path $msi -Type Leaf))
{
throw "The installer could not be found: '$msi'"
}
$name = (Get-Item $msi).BaseName
Write-Host "Installing $name"
$p =
#(
"/I `"$msi`"", # Install this MSI
"/QN", # Quietly, without a UI
"/L*V `"$ENV:TEMP\$name.log`"" # Verbose output to this log
)
Start-Process -FilePath "msiexec" -ArgumentList $p -Wait
}
Where I want to get fancy is with the log output from msiexec. I want to stream the contents of the log to the console while the installer is running. I'm guessing there are multiple parts to the solution
Running the installer in a background, waitable manner
Tailing the log file until some condition is met (installer job completes)
Optional: Filtering the output or writing to debug/verbose for fun
function Start-FileTail {
param($path)
# Get unique source ID
$sourceID = "FileTailLine-" + [guid]::NewGuid()
$job = Start-Job -ArgumentList $path, $sourceID {
param($path,$sid)
Register-EngineEvent -SourceIdentifier $sid -Forward
do{}until(Test-Path $path)
$fs = New-Object IO.FileStream ($path, [IO.FileMode]::Open,
[IO.FileAccess]::Read, [IO.FileShare]::ReadWrite)
$sr = New-Object IO.StreamReader ($fs)
$lines = #()
while(1) {
$line = $sr.ReadLine()
$lines += $line
# Send after every 100 reads
if($lines.Count -gt 100) {
# Join lines into 1 string
$text = #($lines| where {$_} ) -join "`n"
# Only send if text was found
if($text){New-Event -SourceIdentifier $sid -MessageData $text}
$lines = #()
}
}
}
$event = Register-EngineEvent -SourceIdentifier $sourceID -Action {
Write-Host $event.MessageData
}
New-Object Object|
Add-Member -Name Job -Type NoteProperty -Value $job -PassThru|
Add-Member -Name SourceIdentifier -Type NoteProperty -Value $sourceID -PassThru
}
function Stop-FileTail {
param($TailInfo)
Remove-Job $TailInfo.Job -Force
Unregister-Event -SourceIdentifier $tail.SourceIdentifier
}
You can remove the job, and unregister the event once the install is done.
Change Write-Host to Write-Verbose for -Verbose support
EDIT: I tested my answer while installing an application, and found that it was pretty slow when reading the log file. I updated the Get-Content call to use -ReadCount 100 to send the data as arrays of lines. The Write-Host line was updated to handle the arrays.
I also found that using the -Wait switch on Start-Process caused all of the log output to be written after the install was finished. This can be fixed by using:
$msi = Start-Process -FilePath "msiexec" -ArgumentList $p -PassThru
do{}until($msi.HasExited)
EDIT 2: Hmm, I don't get all of the log file when I use -Wait and -ReadCount together. I put the reading of the log file back to how I originally had it. I'm not sure what to do about the speed yet.
EDIT 3: I've updated the code to use a StreamReader instead of Get-Content, and put the code into functions. You would then call it like:
$path = "$ENV:TEMP\$name.log"
if(Test-Path $path){Remove-Item $path}
$msi = Start-Process -FilePath "msiexec" -ArgumentList $p -PassThru
$tail = Start-FileTail $p
do{}until($msi.HasExited)
sleep 1 # Allow time to finish reading log.
Stop-FileTail $tail