Scanning a .log for specific strings in latest lines using Powershell - 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
}
}
}

Related

prepending a timestamp for note taking that outputs to a text file

Trying to make a note-taking script that gives a timestamp and takes a note then prints it to a file. This script should also not exit until it reads wq! from the last line kinda like vim. I'm having trouble getting the timestamp in the right place. Here is what I have so far. Eventually, I'll get it so that wq! doesn't write or erases when complete... thanks for the help and time of anyone who responds. I prefer PowerShell but don't mind Python.
function Get-TimeStamp {
$timenow = get-date
return get-date -f $TimeNow.ToUniversalTime().ToString("HH:mm'Z'") #prints UTC timestamp
}
do
{
#write-output "$(Get-TimeStamp)" | C:\users\vider\Temp\Notes.txt ??? Maybe indexing?
#Hashtable? But how to get the time when the note is taken...
read-host "Notes" | out-file -FilePath C:\users\vider\Temp\Notes.txt -Append
Start-sleep -milliseconds 250
$choice = Get-Item -Path C:\users\vider\Temp\Notes.txt | Get-Content -Tail 1
} until ($choice -eq 'wq!')
Store the input from Read-Host in variable before outputting it to a file, this way you can add conditional statements to understand if the script should exit the loop or output to the file.
As for getting the timestamp in the right place, capturing the input in a variable as mentioned above, would also make it easier to concatenate the timestamp:
"[$(Get-TimeStamp)] $notes"
See Subexpression operator $( ) for more info on why it is needed in this case.
Here is one example on how you can do it:
function Get-TimeStamp {
(Get-Date).ToUniversalTime().ToString("HH:mm'Z'")
}
do
{
$notes = Read-Host "Notes"
if([string]::IsNullOrWhiteSpace($notes) -or $notes -eq 'wq!') {
# if this was just pressing ENTER or spaces or `wq!`, go to `until`
continue
}
# if we are here we can assume the input was NOT `wq!` or $null \ empty space
"[$(Get-TimeStamp)] $notes" | Out-File -FilePath C:\users\vider\Temp\Notes.txt -Append
Start-sleep -Milliseconds 250
} until ($notes -eq 'wq!')

Powershell find string in log file

I'm completely new to Powershell and trying to accomplish a small task. I'm trying to find a string in the last two lines of a log file, and if the value doesn't match, take and action. If it matches, end. I think that's the simplest way to explain it. :-) I'm searching for certain words, if not found, open notepad, if found, exit. I've pieced some of it together, but I'm not sure how to handle the If False, run notepad portion. Would love some help. Thanks.
if (Test-Path C:\windows\ccm\logs\CcmEval.log) {
Get-Content 'C:\windows\ccm\logs\CcmEval.log' -Tail 2 | Select-String "Updating MDM_ConfigSetting.ClientHealthStatus with value 7" | % { $_ -notmatch "value 7" }
Start-Process C:\windows\notepad.exe
}
You can do that with that with one more if else condition. If condition not matches it will open notepad otherwise it will execute your command to exit.
if (Test-Path C:\windows\ccm\logs\CcmEval.log) {
$condition = Get-Content 'C:\windows\ccm\logs\CcmEval.log' -Tail 2 | Select-String "Updating MDM_ConfigSetting.ClientHealthStatus with value 7" | % { $_ -notmatch "value 7" }
if (!$condition) {
Start-Process C:\windows\notepad.exe
}
else {
"Do something here"
}
}
You can drop the Test-Path if you add -ErrorAction SilentlyContinue to the Get-Content, so it will quietly return nothing if the file is not found. This means you can use the same code whether or not the file exists.
-match takes a regular expression pattern, which is powerful enough that you can build "not 7" into it in one go using [^7] to mean "any character except 7".
$logFile = 'C:\windows\ccm\logs\CcmEval.log'
$lines = Get-Content -Path $logFile -Tail 2 -ErrorAction SilentlyContinue
if ($lines -match "Updating MDM_ConfigSetting.ClientHealthStatus with value [^7]") {
Start-Process C:\windows\notepad.exe
}

Combine outputs in Powershell

I currently have this script that checks the registry and if the key exists then it will output a value to the console.
How can I modify this script so that it saves each output to a variable and then that variable will be exported to a text/csv file?
if ((Get-ItemPropertyValue -Path "HKLM:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_HTTP_USERNAME_PASSWORD_DISABLE" -Name HelpPane.exe) -eq '1')
{
Write-Output 'Yes'
}
else
{
Write-Output 'No'
}
if ((Get-ItemPropertyValue -Path "HKLM:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_DISABLE_SQM_UPLOAD_FOR_APP" -Name iexplore.exe) -eq '1')
{
Write-Output 'Yes'
}
else
{
Write-Output 'No'
}
if ($Host.Name -eq "ConsoleHost")
{
Write-Host "Press any key to continue..."
$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp") > $null
Use Tee-Object for this, which moves data through the pipeline as well as saves it to a file:
$content | Tee-Object -FilePath C:\some\path\on\disk.txt
This will take your variable $content, pipe it to Tee-Object which writes the output to a file, then takes the same output and pushes it through the pipeline. You should see that $content is also written to the output stream in this case but you could also pass it to another cmdlet in the pipeline if you choose to do so.
You have options.
3 ways to store and display PowerShell Variable simultaneously
https://ridicurious.com/2017/06/30/3-ways-to-store-display-results-infrom-a-powershell-variable-at-the-same-time
# Using -OutVariable parameter
Get-Process a* -OutVariable process
# PowerShell Variable squeezing
($process = Get-Process a*)
# Using Tee-Object Cmdlet
Tee-Object Cmdlet T’s results to o/p stream and Variable $process at the same time
Point of note:
Avoid using Write-Host/echo, unless you are using screen text coloring. There is little reason to use it as output to the screen is the PowerShell default.
Also, if you are planning to use data down the line/ pipe, etc, then Write-Host empties the buffer and the data is gone. Well depending on what version of PowerShell you are using.
Resources:
From the creator of Powershell.
Write-Host Considered Harmful
http://www.jsnover.com/blog/2013/12/07/write-host-considered-harmful
... Jeffrey Snover changes his stance on this as of May 2016.
With PowerShell v5 Write-Host no longer "kills puppies". data is
captured into info stream ...
https://twitter.com/jsnover/status/727902887183966208
https://learn.microsoft.com/en-us/powershell/module/Microsoft.PowerShell.Utility/Write-Information?view=powershell-5.1
Your code without the Write-Host thing.
if ((Get-ItemPropertyValue -Path 'HKLM:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_HTTP_USERNAME_PASSWORD_DISABLE' -Name HelpPane.exe) -eq '1')
{'Yes'}
else {'No'}
if ((Get-ItemPropertyValue -Path 'HKLM:\Software\Microsoft\Internet Explorer\Main\FeatureControl\FEATURE_DISABLE_SQM_UPLOAD_FOR_APP' -Name iexplore.exe) -eq '1')
{'Yes'}
else { 'No'}
if ($Host.Name -eq "ConsoleHost")
{
'Press any key to continue...'
$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyUp') > $null
}
Lastly, be cognizant about quoting. Single quotes for simple strings, and double quotes for variable expansion or other specific string handling.
As defined in the help files and other resources:
about_Quoting_Rules - PowerShell | Microsoft Docs
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
A Story of PowerShell Quoting Rules
https://trevorsullivan.net/2016/07/20/powershell-quoting
Windows PowerShell Quotes
https://www.computerperformance.co.uk/powershell/quotes

Powershell script to determine when a program is quit

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

Powershell Get-Content with Wait flag and IOErrors

I have a PowerShell script that spawns x number of other PowerShell scripts in a Fire-And-Forget way.
In order to keep track of the progress of all the scripts that I just start, I create a temp file, where I have all of them write log messages in json format to report progress.
In the parent script I then monitor that log file using Get-Content -Wait. Whenever I receive a line in the log file, I parse the json and update an array of objects that I then display using Format-Table. That way I can see how far the different scripts are in their process and if they fail at a specific step. That works well... almost.
I keep running into IOErrors because so many scripts are accessing the log file, and when that happens the script just aborts and I lose all information on what is going on.
I would be able to live with the spawned scripts running into an IOError because they just continue and then I just catch the next message. I can live with some messages getting lost as this is not an audit log, but just a progress log.
But when the script that tails the log crashes then I lose insight.
I have tried to wrap this in a Try/Catch but that doesn't help. I have tried setting -ErrorAction Stop inside the Try/Catch but that still doesn't catch the error.
My script that reads looks like this:
function WatchLogFile($statusFile)
{
Write-Host "Tailing statusfile: $($statusFile)"
Write-Host "Press CTRL-C to end."
Write-Host ""
Try {
Get-Content $statusFile -Force -Wait |
ForEach {
$logMsg = $_ | ConvertFrom-JSON
#Update status on step for specific service
$svc = $services | Where-Object {$_.Service -eq $logMsg.Service}
$svc.psobject.properties[$logMsg.step].value = $logMsg.status
Clear-Host
$services | Format-Table -Property Service,Old,New,CleanRepo,NuGet,Analyzers,CleanImports,Build,Invoke,Done,LastFailure
} -ErrorAction Stop
} Catch {
WatchLogFile $statusFile
}
}
And updates are written like this in the spawned scripts
Add-Content $statusFile $jsonLogMessage
Is there an easy way to add retries or how can I make sure my script survives file locks?
As #ChiliYago pointed out I should use jobs. So that is what I have done now. I had to figure out how to get the output as it arrived from the many scripts.
So I did added all my jobs to an array of jobs and and monitored them like this. Beware that you can receive multiple lines if your script has had multiple outputs since you invoked Receive-Job. Be sure to use Write-Output from the scripts you execute as jobs.
$jobs=#()
foreach ($script in $scripts)
{
$sb = [scriptblock]::create("$script $(&{$args} #jobArgs)")
$jobs += Start-Job -ScriptBlock $sb
}
while ($hasRunningJobs -gt 0)
{
$runningJobs = $jobs | Where-Object {$_.State -eq "Running"} | measure
$hasRunningJobs = $runningJobs.Count
foreach ($job in $jobs)
{
$outvar = Receive-Job -Job $job
if ($outvar)
{
$outvar -split "`n" | %{ UpdateStatusTable $_}
}
}
}
Write-Host "All scripts done."