Powershell script to determine when a program is quit - powershell

I have two scripts I would like to combine, but the second script can't begin until a program (Photoshop) is closed. Script one ends by starting a photoshop script with Invoke-Item. Once the Photoshop script is complete PhotoShop closes. The second code archives the raw files with a simple Move-Item. With PowerShell, how can I know when PhotoShop is closed and begin my Move-Item?
I have spent some time researching this to see what documentation there is, but either I am asking my questions poorly or it is an obscure enough I can't find any leads to begin off of.
# Script One
ii "E:\resizerScript.jsx"
#Something to determine when PhotoShop is closed and begin the next bit of code.
# Script Two
Move-Item -path "E:\Staged\*" -Destination "E:\Archived"
I'm very new to coding and what I have is cobbled together from other articles. If anything is too unclear I would be happy to elaborate. Thanks in advance for any help or direction.

You can use Wait-Process,
Invoke-Item "E:\resizerScript.jsx"
Wait-Process photoshop
Move-Item -Path "E:\Staged\*" -Destination "E:\Archived"
but I recommend using Start-Process -Wait to start Photoshop.
$photoshopPath = "C:\...\Photoshop.exe"
Start-Process $photoshopPath "E:\resizerScript.jsx" -Wait
Move-Item -Path "E:\Staged\*" -Destination "E:\Archived"
If you want to set the timeout:
Start-Process $photoshopPath "E:\resizerScript.jsx" -PassThru |
Wait-Process -Timeout (15 * 60) -ErrorAction Stop

First, you need to find photoshop's process name. Open powershell and run
Get-Process | Select-Object -Property ProcessName
Then use the following (you can customize it according to your needs of course, I've tested using Outlook)
param(
[string]$procName = "Outlook",
[int]$timeout = 90, ## seconds
[int]$retryInterval = 1 ## seconds
)
$isProcActive = $true
$timer = [Diagnostics.Stopwatch]::StartNew()
# to check the process' name:
# Get-Process | Select-Object -Property ProcessName
while (($timer.Elapsed.TotalSeconds -lt $timeout) -and ($isProcActive)) {
$procId = (Get-Process | Where-Object -Property ProcessName -EQ $procName).Id
if ([string]::IsNullOrEmpty($procId))
{
Write-Host "$procName is finished"
$isProcActive = $false
}
}
$timer.Stop()
if ($isProcActive)
{
Write-Host "$procName did not finish on time, aborting operation..."
# maybe you want to kill it?
# Stop-Process -Name $procName
exit
}
# do whatever
[UPDATE] if you need to put this inside another script, you need to omit the param since this must be the 1st thing in a script. So it would look like:
# start of script
$procName = "Outlook"
$timeout = 90 ## seconds
$retryInterval = 1 ## seconds
$isProcActive = $true
# etc etc
Hope this helps,
Jim

Related

Add the result of a Powershell Start-Process to a file instead of replacing it with -RedirectStandardOutput

I use the following command in Powershell to convert files in the background but would like to log the results all in one file. Now the -RedirectStandardOutput replaces the file each run.
foreach ($l in gc ./files.txt) {Start-Process -FilePath "c:\Program Files (x86)\calibre2\ebook-convert.exe" -Argumentlist "'$l' '$l.epub'" -Wait -WindowStyle Hidden -RedirectStandardOutput log.txt}
I tried with a redirect but then the log is empty.
If possible I would like to keep it a one-liner.
foreach ($l in gc ./files.txt) {Start-Process -FilePath "c:\Program Files (x86)\calibre2\ebook-convert.exe" -Argumentlist "`"$l`" `"$l.epub`"" -Wait -WindowStyle Hidden *> log.txt}
If sequential, synchronous execution is acceptable, you can simplify your command to use a single output redirection (the assumption is that ebook-convert.exe is a console-subsystem application, which PowerShell therefore executes synchronously (in a blocking manner).:
Get-Content ./files.txt | ForEach-Object {
& 'c:\Program Files (x86)\calibre2\ebook-convert.exe' $_ "$_.epub"
} *> log.txt
Placing * before > tells PowerShell to redirect all output streams, which in the case of external programs means both stdout and stderr.
If you want to control the character encoding, use Out-File - which > effectively is an alias for - with its -Encoding parameter; or, preferably, with text output - which external-program output always is in PowerShell - Set-Content. To also capture stderr output, append *>&1 to the command in the pipeline segment before the Out-File / Set-Content call.
Note that PowerShell never passes raw output from external programs through to files - they are first always decoded into .NET strings, based on the encoding stored in [Console]::OutputEncoding (the system's active legacy OEM code page by default), and then re-encoded on saving to a file, using the file-writing cmdlet's own defaults, unless overridden with -Encoding - see this answer for more information.
If you want asynchronous, parallel execution (such as via Start-Process, which is asynchronous by default), your best bet is to:
write to separate (temporary) files:
Pass a different output file to -RedirectStandardOutput / -RedirectStandardError in each invocation.
Note that if you want to merge stdout and stderr output and capture it in the same file, you'll have to call your .exe file via a shell (possibly another PowerShell instance) and use its redirection features; for PowerShell, it would be *>log.txt; for cmd.exe (as shown below), it would be > log.txt 2>&1
wait for all launched processes to finish:
Pass -PassThru to Start-Process and collect the process-information objects returned.
Then use Wait-Process to wait for all processes to terminate; use the -Timeout parameter as needed.
and then merge them into a single log file.
Here's an implementation:
$procsAndLogFiles =
Get-Content ./files.txt | ForEach-Object -Begin { $i = 0 } {
# Create a distinct log file for each process,
# and return its name along with a process-information object representing
# each process as a custom object.
$logFile = 'log{0:000}.txt' -f ++$i
[pscustomobject] #{
LogFile = $logFile
Process = Start-Process -PassThru -WindowStyle Hidden `
-FilePath 'cmd.exe' `
-Argumentlist "/c `"`"c:\Program Files (x86)\calibre2\ebook-convert.exe`" `"$_`" `"$_.epub`" >`"$logFile`" 2>&1`""
}
}
# Wait for all processes to terminate.
# Add -Timeout and error handling as needed.
$procsAndLogFiles.Process | Wait-Process
# Merge all log files.
Get-Content -LiteralPath $procsAndLogFiles.LogFile > log.txt
# Clean up.
Remove-Item -LiteralPath $procsAndLogFiles.LogFile
If you want throttled parallel execution, so as to limit how many background processes can run at a time:
# Limit how many background processes may run in parallel at most.
$maxParallelProcesses = 10
# Initialize the log file.
# Use -Force to unconditionally replace an existing file.
New-Item log.txt
# Initialize the list in which those input files whose conversion
# failed due to timing out are recorded.
$allTimedOutFiles = [System.Collections.Generic.List[string]]::new()
# Process the input files in batches of $maxParallelProcesses
Get-Content -ReadCount $maxParallelProcesses ./files.txt |
ForEach-Object {
$i = 0
$launchInfos = foreach ($file in $_) {
# Create a distinct log file for each process,
# and return its name along with the input file name / path, and
# a process-information object representing each process, as a custom object.
$logFile = 'log{0:000}.txt' -f ++$i
[pscustomobject] #{
InputFile = $file
LogFile = $logFile
Process = Start-Process -PassThru -WindowStyle Hidden `
-FilePath 'cmd.exe' `
-ArgumentList "/c `"`"c:\Program Files (x86)\calibre2\ebook-convert.exe`" `"$file`" `"$_.epub`" >`"$file`" 2>&1`""
}
}
# Wait for the processes to terminate, with a timeout.
$launchInfos.Process | Wait-Process -Timeout 30 -ErrorAction SilentlyContinue -ErrorVariable errs
# If not all processes terminated within the timeout period,
# forcefully terminate those that didn't.
if ($errs) {
$timedOut = $launchInfos | Where-Object { -not $_.Process.HasExited }
Write-Warning "Conversion of the following input files timed out; the processes will killed:`n$($timedOut.InputFile)"
$timedOut.Process | Stop-Process -Force
$allTimedOutFiles.AddRange(#($timedOut.InputFile))
}
# Merge all temp. log files and append to the overall log file.
$tempLogFiles = Get-Content -ErrorAction Ignore -LiteralPath ($launchInfos.LogFile | Sort-Object)
$tempLogFiles | Get-Content >> log.txt
# Clean up.
$tempLogFiles | Remove-Item
}
# * log.txt now contains all combined logs
# * $allTimedOutFiles now contains all input file names / paths
# whose conversion was aborted due to timing out.
Note that the above throttling technique isn't optimal, because each batch of inputs is waited for together, at which point the next batch is started. A better approach is to launch a new process as soon as one of the available parallel "slots" up, as shown in the next section; however, note that PowerShell (Core) 7+ is required.
PowerShell (Core) 7+: Efficiently throttled parallel execution, using ForEach-Object -Parallel:
PowerShell (Core) 7+ introduced thread-based parallelism to the ForEach-Object cmdlet, via the -Parallel parameter, which has built-in throttling that defaults to a maximum of 5 threads by default, but can be controlled explicitly via the -ThrottleLimit parameter.
This enables efficient throttling, as a new thread is started as soon as an available slot opens up.
The following is a self-contained example that demonstrates the technique; it works on both Windows and Unix-like platforms:
Inputs are 9 integers, and the conversion process is simulated simply by sleeping a random number of seconds between 1 and 9, followed by echoing the input number.
A timeout of 6 seconds is applied to each child process, meaning that a random number of child processes will time out and be killed.
#requires -Version 7
# Use ForEach-Object -Parallel to launch child processes in parallel,
# limiting the number of parallel threads (from which the child processes are
# launched) via -ThrottleLimit.
# -AsJob returns a single job whose child jobs track the threads created.
$job =
1..9 | ForEach-Object -ThrottleLimit 3 -AsJob -Parallel {
# Determine a temporary, thread-specific log file name.
$logFile = 'log_{0:000}.txt' -f $_
# Pick a radom sleep time that may or may not be smaller than the timeout period.
$sleepTime = Get-Random -Minimum 1 -Maximum 9
# Launch the external program asynchronously and save information about
# the newly launched child process.
if ($env:OS -eq 'Windows_NT') {
$ps = Start-Process -PassThru -WindowStyle Hidden cmd.exe "/c `"timeout $sleepTime >NUL & echo $_ >$logFile 2>&1`""
}
else { # macOS, Linux
$ps = Start-Process -PassThru sh "-c `"{ sleep $sleepTime; echo $_; } >$logFile 2>&1`""
}
# Wait for the child process to exit within a given timeout period.
$ps | Wait-Process -Timeout 6 -ErrorAction SilentlyContinue
# Check if a timout has occurred (implied by the process not having exited yet)
$timedOut = -not $ps.HasExited
if ($timedOut) {
# Note: Only [Console]::WriteLine produces immediate output, directly to the display.
[Console]::WriteLine("Warning: Conversion timed out for: $_")
# Kill the timed-out process.
$ps | Stop-Process -Force
}
# Construct and output a custom object that indicates the input at hand,
# the associated log file, and whether a timeout occurred.
[pscustomobject] #{
InputFile = $_
LogFile = $logFile
TimedOut = $timedOut
}
}
# Wait for all child processes to exit or be killed
$processInfos = $job | Receive-Job -Wait -AutoRemoveJob
# Merge all temporary log files into an overall log file.
$tempLogFiles = Get-Item -ErrorAction Ignore -LiteralPath ($processInfos.LogFile | Sort-Object)
$tempLogFiles | Get-Content > log.txt
# Clean up the temporary log files.
$tempLogFiles | Remove-Item
# To illustrate the results, show the overall log file's content
# and which inputs caused timeouts.
[pscustomobject] #{
CombinedLogContent = Get-Content -Raw log.txt
InputsThatFailed = ($processInfos | Where-Object TimedOut).InputFile
} | Format-List
# Clean up the overall log file.
Remove-Item log.txt
You can use redirection and append to files if you don't use Start-Process, but a direct invocation:
foreach ($l in gc ./files.txt) {& 'C:\Program Files (x86)\calibre2\ebook-convert.exe' "$l" "$l.epub" *>> log.txt}
For the moment I'm using an adaption on mklement0's answer.
ebook-convert.exe often hangs so I need to close it down if the process takes longer than the designated time.
This needs to run asynchronous because the number of files and the processor time taken (5 to 25% depending on the conversion).
The timeout needs to be per file, not on the whole of the jobs.
$procsAndLogFiles =
Get-Content ./files.txt | ForEach-Object -Begin { $i = 0 } {
# Create a distinct log file for each process,
# and return its name along with a process-information object representing
# each process as a custom object.
$logFile = 'd:\temp\log{0:000}.txt' -f ++$i
Write-Host "$(Get-Date) $_"
[pscustomobject] #{
LogFile = $logFile
Process = Start-Process `
-PassThru `
-FilePath "c:\Program Files (x86)\calibre2\ebook-convert.exe" `
-Argumentlist "`"$_`" `"$_.epub`"" `
-WindowStyle Hidden `
-RedirectStandardOutput $logFile `
| Wait-Process -Timeout 30
}
}
# Wait for all processes to terminate.
# Add -Timeout and error handling as needed.
$procsAndLogFiles.Process
# Merge all log files.
Get-Content -LiteralPath $procsAndLogFiles.LogFile > log.txt
# Clean up.
Remove-Item -LiteralPath $procsAndLogFiles.LogFile
Since the problem in my other answer was not completely solved (not killing all the processes that take longer than the timeout limit) I rewrote it in Ruby.
It's not powershell but if you land on this question and also know Ruby (or not) it could help you.
I believe it's the use of Threads that solves the killing issue.
require 'logger'
LOG = Logger.new("log.txt")
PROGRAM = 'c:\Program Files (x86)\calibre2\ebook-convert.exe'
LIST = 'E:\ebooks\english\_convert\mobi\files.txt'
TIMEOUT = 30
MAXTHREADS = 6
def run file, log: nil
output = ""
command = %Q{"#{PROGRAM}" "#{file}" "#{file}.epub" 2>&1}
IO.popen(command+" 2>&1") do |io|
begin
while (line=io.gets) do
output += line
log.info line.chomp if log
end
rescue => ex
log.error ex.message
system("taskkill /f /pid #{io.pid}") rescue log.error $#
end
end
if File.exist? "#{file}.epub"
puts "converted #{file}.epub"
File.delete(file)
else
puts "error #{file}"
end
output
end
threads = []
File.readlines(LIST).each do |file|
file.chomp! # remove line feed
# some checks
if !File.exist? file
puts "not found #{file}"
next
end
if File.exist? "#{file}.epub"
puts "skipping #{file}"
File.delete(file) if File.exist? file
next
end
# go on with the conversion
thread = Thread.new {run(file, log: LOG)}
threads << thread
next if threads.length < MAXTHREADS
threads.each do |t|
t.join(TIMEOUT)
unless t.alive?
t.kill
threads.delete(t)
end
end
end

Why is other powershell script run twice?

I have a script that checks whether disk has a certain amount of free space. If not, a pop-up appears asking for a yes or no. If yes, then an alarm is set to 1 and then another script that deletes files from folder runs. My issue is that it seems to delete twice the number specified in the script.
Main script:
$limit_low = 0.1 # låg gräns 10%
$DiskD = Get-PSDrive D | Select-Object Used,Free | Write-Output
$DiskD_use = [math]::Round(($DiskD.Free / ($DiskD.Used + $DiskD.Free)),2)
if( $DiskD_use -le $limit_low ) {
Write-Host "RDS-server har för lite utrymme på disk D $diskD_use < $limit_low" -ForegroundColor Red -BackgroundColor Yellow
$ButtonType = 4
$Timeout = 60
$Confirmation = New-Object -ComObject wscript.shell
$ConfirmationAnswer = $Confirmation.popup("Clear disk space?",$Timeout,"No space",$ButtonType)
If( $ConfirmationAnswer -eq 6 ) {
Write-Host "Kör script Diskspace.ps1 under P:\backupscripts"
& c:\dynamics\app\JDSend.exe "/UDP /LOG:c:\dynamics\app\Fixskick.log /TAG:Lunsc2:K_PROCESS_LARM_DISKUTRYMME "1""
& P:\BackupScripts\Delete_archives_test.ps1 # here i call the other script
} else {
Write-Host "Gör ingenting"
& c:\dynamics\app\JDSend.exe "/UDP /LOG:c:\dynamics\app\Fixskick.log /TAG:Lunsc2:K_PROCESS_LARM_DISKUTRYMME "0""
}
}
Other script:
# List all txt-files in directory, sort them och select the first 10, then delete
Get-ChildItem -Path c:\temp -File Archive*.txt | Sort-Object | Select-Object -First 10 | Remove-Item -Force
Cheers
EDIT
So it would be enough to enclose the first statement like this:
(Get-ChildItem -Path c:\temp -File Archive*.txt) | Sort-Object | Select-Object -First 10 | Remove-Item -Force
?? Funny thing is i tried to reproduce this today at home with no effect. Works as intended from here, even without parens.
Further I am painfully ignorant about how to use foreach statement, as it doesn't pipe the bastard :)
foreach ($file in $filepath) {$file} | Sort-Object | Select-Object -First 10 | Remove-Item -Force
Tried to put the sort and select-part in the {} too, but nothing good came of it. As i'm stuck in the pipe and don't understand the foreach logic.
your problem appears to be caused by how your pipeline works. [grin]
think about what it does ...
read ONE fileinfo item
send it to the pipeline
change/add a file
continue the pipeline
that 3rd step will cause the file list to change ... and may result in a file being read again OR some other change in the list of files to work on.
there are two solutions that come to mind ...
wrap the Get-ChildItem call in parens
that will force one read of the list before sending anything to the pipeline ... and that will ignore any changes caused by later pipeline stages.
use a foreach loop
that will read the whole list and then iterate thru the list one item at a time.
the 2nd solution also has the benefit of being easier to debug since the value of your current item only changes when explicitly modified. the current pipeline item changes at every pipeline stage ... and that is easy to forget. [grin]

Scanning a .log for specific strings in latest lines using Powershell

I have a .log file that constantly adds lines to itself and I am trying to make a Powershell script that will launch 1 of two batch scripts when the respective string of characters is detected in the latest line of the .log file. Here's what I have so far:
while ($True) {
Write-Output 'Enter <ctrl><c> to break out of this loop.'
Start-Sleep -Seconds 1
Copy-Item -LiteralPath "C:\LocationOfFile\latest.log" -Destination "C:\Users\Diogo\Desktop\Detector"
Rename-Item -Path "C:\Users\Diogo\Desktop\Detector\latest.log" -NewName "latest.txt"
Get-Content -Path "latest.txt" -tail 1 -wait | Select-String -Quiet '§6§lP§e§lrof'
if (System.Boolean -eq True) {
Invoke-Item result1.bat
Read-Host -Prompt "Press Enter to continue"
}
else {
Get-Content -Path "latest.txt" -tail 1 -wait | Select-String -Quiet 'spawned'
if (System.Boolean -eq True) {
Invoke-Item result2.bat
Read-Host -Prompt "Press Enter to continue"
}
else {
}
}
}
I first copy the .log file from it's location and Change it into a .txt.
Then I search for the strings ("§6§lP§e§lrof" and "spawned")
And finally I try to get it to do it over again, but this doesn't seem to be working as well as the seearching.
Any help?
Thanks in advance <3
EDIT:
Thank you so much for the comprehensive reply, that really helped me grasp some Powershell concepts and it worked flawlessly. The second script was a tiny overkill tho, I actually have the exact opposite problem: the lines are added quite slowly. In a perfect world I want the script to keep going after finding one result and not have me keep resetting it after each result found. There is another rule about the log file that is really interesting: Lines with the strings I'm after never occur one after another, there is always one in between, at least. This means if the script finds the same string twice in a row, it's just the same line and I don't want my batch script to go off. The PowerShell script I am using right now (which is the code you showed me with minor changes to make it loop) is at the end and it is working with only a single small hiccup: If I'm using my computer for something else Powershell becomes the window on top when it finds a result and I would like that not to happen, could you help me with that last thing? Thank you very much in advance!
while ($True) {
Write-Output 'Enter <ctrl><c> to break out of this loop.'
Start-Sleep -Seconds 1
$LastLogLine = Get-Content -Path "C:\LocationOfFile\latest.log" -tail 1
if ($LastLogLine -ne $LastLine) {
if ($LastLogLine -like '*§6§lP§e§lrof*') {
Start-Process -FilePath "result1.bat" -WindowStyle Minimized
$LastLine = $LastLogLine
} elseif ($LastLogLine -like '*spawned*') {
Start-Process -FilePath "result2.bat" -WindowStyle Minimized
$LastLine = $LastLogLine
}
}
}
First off, your script doesn't work for two reasons:
Get-Content -Path "latest.txt" -tail 1 -wait | Select-String -Quiet '§6§lP§e§lrof'
Get-Content -Wait will keep running as long as the file it reads exists or until it gets Ctrl-C'd, so your script will never go beyond that. You can just remove -Wait here.
if (System.Boolean -eq True)
I don't see what you're trying to do here. Collect the results from the previous Select-String ? Select-String does not set any variable or flag on it's own. Also, you're comparing a type to a string: you're asking "is the concept of a boolean equal to the string 'True' ?". What you can do is store the result of Select-String and just do if ($Result -eq $True) (emphasis on $True, not "True").
Additionally, a couple things I would rewrite or correct in your script:
Copy-Item every second: Is it necessary ? Why not just read the original file and store it in a variable ? If it is just so you can change the extension from .log to .txt, know that powershell does not care about the extension and will happily read anything.
Select-String: have you considered just using the comparison operator -like, as in if ($MyString -like "*$MyKeyword*") {...} ?
If blocks do not need an Else block. If your Else does nothing, you can just not write it. And there is an elseif block that you can use instead of chaining an else and an if.
Code style: Please pick an indentation style and stick to it. The one I see most of the time is 1TBS, but K&R or Allman are well known too. I may or may not have requested an edit to get some indentation on your question :p
So, we end up with this:
while ($True) {
Write-Output 'Enter <ctrl><c> to break out of this loop.'
Start-Sleep -Seconds 1
$LastLogLine = Get-Content -Path "C:\LocationOfFile\latest.log" -tail 1
if ($LastLogLine -like '*§6§lP§e§lrof*') {
Invoke-Item result1.bat
Read-Host -Prompt "Press Enter to continue"
} elseif ($LastLogLine -like '*spawned*') {
Invoke-Item result2.bat
Read-Host -Prompt "Press Enter to continue"
}
}
However, this will not work if the program that writes your logs can write faster than you can process the lines, batch script included. If it does that, your script will skip lines as you only handle the last line. If two lines get written you won't see the second to last.
To solve that, we can do a bit of asynchronous magic using Powershell jobs, and we'll be able to see all lines written since the last loop, be it 1 line written, 0 lines, or 100 lines. about_jobs is a very good primer on Powershell jobs and asynchronous operations, read it.
$stream = Start-Job -ArgumentList $LogPath -Name "StreamFileContent" -ScriptBlock {Get-Content $args -Wait}
Receive-Job -Job $Stream # Discard everything that was already written in the file, we only want the stuff that is added to the file after we've started.
while($true) { # As long as the script is left running
foreach($NewLine in (Receive-Job -Job $stream)) { # Fetch the lines that Get-Content gave us since last loop
if ($NewLine -like '*§6§lP§e§lrof*') { # Check for your keyword
C:\MyScriptPath\MyScript1.bat # Start batch script
} elseif ($NewLine -like '*spawned*') {
C:\MyScriptPath\MyScript2.bat
}
}
}

Get-WindowsUpdateLog stream re-direction

Has anyone noticed how the Get-WindowsUpdateLog cmdlet cannot be redirected to any streams?
Furthermore, storing the output into a variable, piping it, or any type of re-direction leads to the cmdlet to only be executed.
Any help redirecting/silencing the output of this command would be appreciated.
What I've tried:
Get-WindowsUpdateLog | Out-Null
Get-WindowsUpdateLog > $null
$sink = Get-WindowsUpdateLog
Everything I could find failed to suppress the output of the CmdLet Get-WindowsUpdateLog. As you say, the displayed information in the console is not properly following the output streams as we know them in PowerShell.
The only workaround I found is using Jobs:
$Job = Start-Job -ScriptBlock {Get-WindowsUpdateLog}
$Job | Wait-Job | Remove-Job
This way all output is handled within the job and we don't retrieve the result. It's also unnecessary to retrieve it as the result is simply a text file placed in the -Path parameter.
As an addendum to DarkLite's answer, we can also utilise the following code to check if Get-WindowsUpdateLog cmdlet worked properly :
# Generate a unique filename.
$o_filename = "C:\temp\" + "WindowsUpdateLog" + "_" + ( Get-Date -UFormat "%H_%M_%S" ) + ".txt"
$o_job = Start-Job -ScriptBlock { Get-WindowsUpdateLog -LogPath "$( $args[0] )" } `
-ArgumentList $o_filename -ErrorAction SilentlyContinue
Start-Sleep -Seconds 5
$o_job | Remove-Job -ErrorAction SilentlyContinue
if( ! ( Test-Path $o_filename ) ) {
# Return an exception
}
else {
# Do some work on the generated log file
}
I think the lesson here is don't use Out-Default in a script. https://keithga.wordpress.com/2018/04/03/out-default-considered-harmful/ I don't see that command in powershell 6. But it is in powershell 7!

Powershell read file live and output to speech

What i am looking for is to take powershell and read the file content out to the speech synthesis module.
File name for this example will be read.txt.
Start of the Speech module:
Add-Type -AssemblyName System.speech
$Narrator1 = New-Object System.Speech.Synthesis.SpeechSynthesizer
$Narrator1.SelectVoice('Microsoft Zira Desktop')
$Narrator1.Rate = 2
$Location = "$env:userprofile\Desktop\read.txt"
$Contents = Get-Content $Location
Get-Content $Location -wait -Tail 2 | where {$Narrator1.Speak($Contents)}
This works once. I like to use the Clear-Content to wipe the read.txt after each initial read and have powershell wait until new line is added to the read.txt file then process it again to speak the content. I believe I can also make it run in the background with -windowstyle hidden
Thank you in advanced for any assistance.
Scott
I don't think a loop is the answer, I would use the FileSystemWatcher to detect when the file has changed. Try this:
$fsw = New-Object System.IO.FileSystemWatcher
$fsw.Path = "$env:userprofile\Desktop"
$fsw.Filter = 'read.txt'
Register-ObjectEvent -InputObject $fsw -EventName Changed -Action {
Add-Type -AssemblyName System.speech
$Narrator1 = New-Object System.Speech.Synthesis.SpeechSynthesizer
$Narrator1.SelectVoice('Microsoft Zira Desktop')
$Narrator1.Rate = 2
$file = $Event.SourceEventArgs.FullPath
$Contents = Get-Content $file
$Narrator1.Speak($Contents)
}
Your only problem was that you accidentally used the previously assigned $Contents variable in the where (Where-Object) script block rather than $_, the automatic variable representing the current pipeline object:
Get-Content $Location -Wait -Tail 2 | Where-Object { $Narrator1.Speak($_) }
Get-Content $Location -Wait will poll the input file ($Location here) every second to check for new content and pass it through the pipeline (the -Tail argument only applies to the initial reading of the file; as new lines are added, they are all passed through).
The pipeline will stay alive indefinitely - until you delete the $Location file or abort processing.
Since the command is blocking, you obviously need another session / process to add content to file $Location, such as another PowerShell window or a text editor that has the file open and modifies its content.
You can keep appending to the file with >>, but that will keep growing it.
To discard the file's previous content, you must indeed use Clear-Content, as you say, which truncates the existing file without recreating it, and therefore keeps the pipeline alive; e.g.:
Clear-Content $Location
'another line to speak' > $Location
Caveat: Special chars. such as ! and ? seem to cause silent failure to speak. If anyone knows why, do tell us. The docs offer no immediate clues.
As for background operation:
With a background job, curiously, the Clear-Content / > combination appears not to work; if anybody knows why, please tell us.
However, using >> - which grows the file - does work.
The following snippet demonstrates the use of a background job to keep speaking input as it is being added to a specified file (with some delay), until a special end-of-input string is sent:
# Determine the input file (on the user's desktop)
$file = Join-Path ([environment]::GetFolderPath('Desktop')) 'read.txt'
# Initialize the input file.
$null > $file
# Define a special string that acts as the end-of-input marker.
$eofMarker = '[quit]'
# Start the background job (PSv3+ syntax)
$job = Start-Job {
Add-Type -AssemblyName System.speech
$Narrator1 = New-Object System.Speech.Synthesis.SpeechSynthesizer
$Narrator1.SelectVoice('Microsoft Zira Desktop')
$Narrator1.Rate = 2
while ($true) { # A dummy loop we can break out of on receiving the end-of-input marker
Get-Content $using:file -Wait | Where-Object {
if ($_ -eq $using:eofMarker) { break } # End-of-input marker received -> exit the pipeline.
$Narrator1.Speak($_)
}
}
# Remove the input file.
Remove-Item -ErrorAction Ignore -LiteralPath $using:file
}
# Speak 1, 2, ..., 10
1..10 | ForEach-Object {
Write-Verbose -Verbose $_
# !! Inexplicably, using Clear-Content followed by > to keep
# !! replacing the file content does *not* work with a background task.
# !! >> - which *appends* to the file - does work, however.
$_ >> $file
}
# Send the end-of-input marker to make the background job stop reading.
$eofMarker >> $file
# Wait for background processing to finish.
# Note: We'll get here long before the background job has finished speaking.
Write-Verbose -Verbose 'Waiting for processing to finish to cleanup...'
$null = Receive-Job $job -wait -AutoRemoveJob