Try/Catch Statements don't seem to be working [duplicate] - function

This question already has an answer here:
How to handle failed variable assignments in powershell? [duplicate]
(1 answer)
Closed 2 years ago.
Afternoon!
EDIT
After some more troubleshooting, I've noticed it doesn't seem to want to catch the error from the New-Item -ItemType Directory -Path Z:\PMO\PMOBits\test section. If I put in a Null value, it catches that error in both my Get-Logger function and using Write-output. Why wouldn't it catch the Folder already exists error?
I'm have a logging function in my PowerShell script and I'm trying to incorporate some error handling using this function. Below is the logging function.
Function Get-Logger {
param(
[Parameter(Mandatory=$true)]
[String]$message,
[Parameter(Mandatory=$false)]
[validatescript({[enum]::getvalues([system.consolecolor]) -contains $_})][string]$Color
)
$TimeStamp = Get-Date -Format "MM-dd-yyy hh:mm:ss"
Write-Host $TimeStamp -NoNewline
IF ($Color) {
Write-Host `t $message -ForegroundColor $($color)
} Else {
Write-Host `t $message -ForegroundColor Cyan
}
$logMessage = "[$TimeStamp] $message"
$logMessage | Out-File -Append -LiteralPath $VerboseLogFile
}
I'm testing this with some simple code for the error handling, see the test code below.
Try { New-Item -ItemType Directory -Path Z:\PMO\PMOBits\test }
Catch {
Get-Logger "ERROR: This file already exists"
}
When I run the script, I see the error populate on the terminal that it already exists, but It's not showing in my log file like the catch section should do.
I've also tried to catch the error without using my function below:
try{...}
catch {
Write-Output $_ | Out-File -Append -LiteralPath $VerboseLogFile
}

Add the -ErrorAction Stop with your New-Item statement.
Try { New-Item -ItemType Directory -Path Z:\PMO\PMOBits\test -ErrorAction Stop }
Catch {
Get-Logger "ERROR: This file already exists"
}

Related

log file variables in function eat up all memory

i have the following simple function which is used several times in a script which iterates through a directory and checks the age of the files in it.
function log($Message) {
$logFilePath = 'C:\logPath\myLog.txt'
$date = Get-Date -Format 'yyyyMMddHHmmss'
$logMessage = "{0}_{1}" -f $date,$Message
if(Test-Path -Path $logFilePath) {
$logFileContent = Get-Content -Path $logFilePath
} else {
$logFileContent = ''
}
$logMessage,$logFileContent | Out-File -FilePath $logFilePath
}
i figured out that this eats up all ram. i don't understand why. i thought the scope of variables in a function are destroyed once the function is run. i fixed the ram issue by adding Remove-Variable logMessage,logFileContent,logFilePath,date to the very end of the function but would like to know how this ram issue could be solved otherwise and why the variables within the function are not destroyed automatically.
Powershell or .Net has a garbage collector, so freeing the memory isn't instant. Garbage Collection in Powershell to Speed Scripts Also the memory management is probably better in Powershell 7. I tried repeating your function many times, but the memory usage didn't go above a few hundred megs.
There's probably some more efficient .net way to prepend a line to a file: Prepend "!" to the beginning of the first line of a file
I have a weird way to prepend a line. I'm not sure how well this would work with a large file. With a 700 meg file, the Working Set memory stayed at 76 megs.
$c:file
one
two
three
$c:file = 'pre',$c:file
$c:file
pre
one
two
three
ps powershell
Handles NPM(K) PM(K) WS(K) CPU(s) Id SI ProcessName
------- ------ ----- ----- ------ -- -- -----------
607 27 67448 76824 1.14 3652 3 powershell
Although as commented, I can hardly believe that this function gobbles up your memory, you could optimize it:
function log ([string]$Message) {
$logFilePath = 'C:\logPath\myLog.txt'
# prefix the message with the current date
$Message = "{0:yyyyMMddHHmmss}_{1}" -f (Get-Date), $Message
if (Test-Path -Path $logFilePath -PathType Leaf) {
# newest log entry on top: append current content
$Message = "{0}`r`n{1}" -f $Message, (Get-Content -Path $logFilePath -Raw)
}
Set-Content -Path $logFilePath -Value $Message
}
I just want to rule out the RAM usage is caused by prepending to the file. Have you tried not storing the log contents in a variable? i.e.
$logMessage,(Get-Content -Path $logFilePath) | Out-File -FilePath $logFilePath
Edit 5/8/20 - Turns out that prepending (when done efficiently) isn't as slow as I thought - it is on the same order as using -Append. However the code (that js2010 pointed to) is long and ugly, but if you really need to prepend to the file, this is the way to do it.
I modified the OP's code a bit to automatically insert a new line.
function log-prepend{
param(
$content,
$filePath = 'C:\temp\myLogP.txt'
)
$file = get-item $filePath
if(!$file.exists){
write-error "$file does not exist";
return;
}
$filepath = $file.fullname;
$tmptoken = (get-location).path + "\_tmpfile" + $file.name;
write-verbose "$tmptoken created to as buffer";
$tfs = [System.io.file]::create($tmptoken);
$fs = [System.IO.File]::Open($file.fullname,[System.IO.FileMode]::Open,[System.IO.FileAccess]::ReadWrite);
try{
$date = Get-Date -Format 'yyyyMMddHHmmss'
$logMessage = "{0}_{1}`r`n" -f $date,$content
$msg = $logMessage.tochararray();
$tfs.write($msg,0,$msg.length);
$fs.position = 0;
$fs.copyTo($tfs);
}
catch{
write-verbose $_.Exception.Message;
}
finally{
$tfs.close();
$fs.close();
if($error.count -eq 0){
write-verbose ("updating $filepath");
[System.io.File]::Delete($filepath);
[System.io.file]::Move($tmptoken,$filepath);
}
else{
$error.clear();
[System.io.file]::Delete($tmptoken);
}
}
}
Here was my original answer that shows how to test the timing using a stopwatch.
When you prepend to a log file, you're reading the entire log file into memory, then writing it back.
You really should be using append - that would make the script run a lot faster.
function log($Message) {
$logFilePath = 'C:\logPath\myLog.txt'
$date = Get-Date -Format 'yyyyMMddHHmmss'
$logMessage = "{0}_{1}" -f $date,$Message
$logMessage | Out-File -FilePath $logFilePath -Append
}
Edit: To convince you that prepending to a log file is a bad idea, here's a test you can do on your own system:
function logAppend($Message) {
$logFilePath = 'C:\temp\myLogA.txt'
$date = Get-Date -Format 'yyyyMMddHHmmss'
$logMessage = "{0}_{1}" -f $date,$Message
$logMessage | Out-File -FilePath $logFilePath -Append
}
function logPrepend($Message) {
$logFilePath = 'C:\temp\myLogP.txt'
$date = Get-Date -Format 'yyyyMMddHHmmss'
$logMessage = "{0}_{1}" -f $date,$Message
if(Test-Path -Path $logFilePath) {
$logFileContent = Get-Content -Path $logFilePath
} else {
$logFileContent = ''
}
$logMessage,$logFileContent | Out-File -FilePath $logFilePath
}
$processes = Get-Process
$stopwatch = [system.diagnostics.stopwatch]::StartNew()
foreach ($p in $processes)
{
logAppend($p.ProcessName)
}
$stopwatch.Stop()
$stopwatch.Elapsed
$stopwatch = [system.diagnostics.stopwatch]::StartNew()
foreach ($p in $processes)
{
logPrepend($p.ProcessName)
}
$stopwatch.Stop()
$stopwatch.Elapsed
I've run this several times until I got a few thousand lines in the log file.
Going from: 1603 to 1925 lines, my results were:
Append: 7.0167008 s
Prepend: 21.7046793 s

Logging to console and file with function passing back return code

I've decided it makes sense that functions being called by a Powershell script should
Log the same output to both a log file and to the console and
should return a status indicating success/failure.
I found a way to do this but it seems ridiculously cumbersome and backwards (illustrated below). I'm thinking this is such a basic and essential capability for any scripting language and that I must be really lost and confused to be doing something in such a backwards way. I'm pretty new to PowerShell but come from a C# background.
I ended up adding -PassThru to every Add-Content statement in the function so the log entry will be coming back in the pipeline as item of an Object[] collection. I then am passing back a final boolean item in the Object[] collection which is the status of the function.
# Main script c:\temp\test1.ps1
Function Write-FunctionOutputToConsole {
Param ([Object[]] $FunctionResults)
foreach ($item in $FunctionResults) {
if ($item -is [System.Array]) {
Write-Host $($item)
}
}
}
Function Get-FunctionReturnCode {
Param ([Object[]] $FunctionResults)
if ($FunctionResults[-1] -is [System.Boolean]) {
Return $FunctionResults[-1]
}
}
. c:\temp\test2.ps1 #pull in external function
$LogFile = "c:\temp\test.log"
$results = FunctionThatDoesStuff -LogFile $LogFile -DesiredReturnValue $true
Write-FunctionOutputToConsole -FunctionResults $results
$FunctionReturnCode = Get-FunctionReturnCode -FunctionResults $results
Add-Content -Path $LogFile -Value "$(Get-Date -Format G) Logging in Main: returnValue=$FunctionReturnCode" -PassThru
# Do some logic based on $FunctionReturnCode
External function
# c:\temp\test2.ps1
function FunctionThatDoesStuff {
Param(
[string] $LogFile,
[bool] $DesiredReturnValue
)
Add-Content -Path $LogFile -Value "-----------------------------------------" -PassThru
Add-Content -Path $LogFile -Value "$(Get-Date -Format G) returnValue=$DesiredReturnValue" -PassThru
Add-Content -Path $LogFile -Value "$(Get-Date -Format G) line 1 being logged" -PassThru
Add-Content -Path $LogFile -Value "$(Get-Date -Format G) line 2 being logged" -PassThru
return $DesiredReturnValue
}
Console Output:
PS C:\Temp> c:\temp\test1.ps1
-----------------------------------------
7/19/2018 3:26:28 PM returnValue=True
7/19/2018 3:26:28 PM line 1 being logged
7/19/2018 3:26:28 PM line 2 being logged
7/19/2018 3:26:28 PM Logging in Main: returnValue=True
Log File
PS C:\Temp> get-content c:\temp\test.log
-----------------------------------------
7/19/2018 3:29:59 PM returnValue=True
7/19/2018 3:29:59 PM line 1 being logged
7/19/2018 3:29:59 PM line 2 being logged
7/19/2018 3:29:59 PM Logging in Main: returnValue=True
As you can see this results in the identical information in the Console and logging file.
I think you're misunderstanding how PowerShell works. For one thing, the information whether or not the last command was successful is automatically stored in the automatic variable $?. In case of an error cmdlets will throw an exception that can be caught for error handling (see also). There is no need to signal success or error status with a return value. Also, PowerShell by default returns all uncaptured output from a function. The return keyword is just for control flow.
I would implement a logging function somewhat like this:
function Write-LogOutput {
[CmdletBinding()]
Param(
[Parameter(Position=0, Mandatory=$true, ValueFromPipeline=$true)]
[string[]]$Message,
[Parameter(Position=1, Mandatory=$false)]
[ValidateScript({Test-Path -LiteralPath $_ -IsValid})]
[string]$LogFile = '.\default.log',
[Parameter(Mandatory=$false)]
[switch]$Quiet,
[Parameter(Mandatory=$false)]
[switch]$PassThru
)
Process {
$Message | ForEach-Object {
$msg = "[{0:yyyy-MM-dd HH:mm:ss}]`t{1}" -f (Get-Date), $_
if (-not $Quiet.IsPresent) {
$msg | Out-Host
}
$msg
} | Add-Content $LogFile
if ($PassThru.IsPresent) {
$Message
}
}
}
and then use it like this:
function FunctionThatDoesStuff {
# ...
# something that should be logged, but not returned
'foo' | Write-LogOutput -LogFile 'C:\path\to\your.log'
# something that should be logged and returned by the function
'bar' | Write-LogOutput -LogFile 'C:\path\to\your.log' -PassThru
# something that should be returned, but not logged
'baz'
# ...
}
$result = FunctionThatDoesStuff
# Output:
# -------
# [2018-07-19 23:44:07] foo
# [2018-07-19 23:44:07] bar
$result
# Output:
# -------
# bar
# baz

Can't write function output to file

I'm currently stuck on finding information on how to easily output the function results to a file.
$Balls = 5
$Plays = 1..26
function PowerBall {
foreach ($Trys in $Plays) {
$random = Get-Random -Input #(1..69) -Count $Balls
Write-Host $random -NoNewline
Write-Host " $Trys" -ForegroundColor Red
}
}
PowerBall | Out-File C:\Windows\Temp\numbers2.txt
Your function is not returning anything because the last line is a Write-Host command. Change your function to this:
function PowerBall {
Foreach($Trys in $Plays) {
$random = get-random -input #(1..69) -count $Balls
Write-Host $random -nonewline
write-host " $Trys" -ForegroundColor Red
"$random $Trys"
}
}
Also if you want to output as a text file then you should use Set-Content instead of Export-Csv.

Functions & powershell remoting

I have a script that works fine but I want to improve my powershell knowledge and would like to know if there is an easier way to do this.....
Part of my script connects to a sever and pulls bak a list of files and sizes on it and exports it to a csv.
I have found a function (Exportcsv) that allows me to append to the csv with earlier versions of powershell.
At the moment I use the invoke-command to remote to each server and the run the script in the script block but this means adding the function each time. So I have the function in my script but then have to repeat it for each server I connect to so it will run remotely
Is there any way to pass the local function to the remote server so I don't have to add to each invoke command.
Invoke-Command –ComputerName server –ScriptBlock {
$wfile = "d:\folder\directorysize_H.csv"
$xfile = "d:\folder\directorysize_F.csv"
function ExportCSV {
[CmdletBinding(DefaultParameterSetName='Delimiter',
SupportsShouldProcess=$true, ConfirmImpact='Medium')]
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[System.Management.Automation.PSObject]
${InputObject},
[Parameter(Mandatory=$true, Position=0)]
[Alias('PSPath')]
[System.String]
${Path},
#region -Append
[Switch]
${Append},
#endregion
[Switch]
${Force},
[Switch]
${NoClobber},
[ValidateSet('Unicode','UTF7','UTF8','ASCII','UTF32',
'BigEndianUnicode','Default','OEM')]
[System.String]
${Encoding},
[Parameter(ParameterSetName='Delimiter', Position=1)]
[ValidateNotNull()]
[System.Char]
${Delimiter},
[Parameter(ParameterSetName='UseCulture')]
[Switch]
${UseCulture},
[Alias('NTI')]
[Switch]
${NoTypeInformation})
begin
{
# This variable will tell us whether we actually need to append
# to existing file
$AppendMode = $false
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Export-Csv',
[System.Management.Automation.CommandTypes]::Cmdlet)
#String variable to become the target command line
$scriptCmdPipeline = ''
# Add new parameter handling
#region Process and remove the Append parameter if it is present
if ($Append) {
$PSBoundParameters.Remove('Append') | Out-Null
if ($Path) {
if (Test-Path $Path) {
# Need to construct new command line
$AppendMode = $true
if ($Encoding.Length -eq 0) {
# ASCII is default encoding for Export-CSV
$Encoding = 'ASCII'
}
# For Append we use ConvertTo-CSV instead of Export
$scriptCmdPipeline += 'ConvertTo-Csv -NoTypeInformation '
# Inherit other CSV convertion parameters
if ( $UseCulture ) {
$scriptCmdPipeline += ' -UseCulture '
}
if ( $Delimiter ) {
$scriptCmdPipeline += " -Delimiter '$Delimiter' "
}
# Skip the first line (the one with the property names)
$scriptCmdPipeline += ' | Foreach-Object {$start=$true}'
$scriptCmdPipeline += '{if ($start) {$start=$false} else {$_}} '
# Add file output
$scriptCmdPipeline += " | Out-File -FilePath '$Path'"
$scriptCmdPipeline += " -Encoding '$Encoding' -Append "
if ($Force) {
$scriptCmdPipeline += ' -Force'
}
if ($NoClobber) {
$scriptCmdPipeline += ' -NoClobber'
}
}
}
}
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
if ( $AppendMode ) {
# redefine command line
$scriptCmd = $ExecutionContext.InvokeCommand.NewScriptBlock(
$scriptCmdPipeline
)
} else {
# execute Export-CSV as we got it because
# either -Append is missing or file does not exist
$scriptCmd = $ExecutionContext.InvokeCommand.NewScriptBlock(
[string]$scriptCmd
)
}
# standard pipeline initialization
$steppablePipeline = $scriptCmd.GetSteppablePipeline(
$myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process
{
try {
$steppablePipeline.Process($_)
} catch {
throw
}
}
end
{
try {
$steppablePipeline.End()
} catch {
throw
}
}
}
Write-Host "Removing old files from xxx"
If (Test-Path $wfile){
Remove-Item $wfile
}
If (Test-Path $xfile){
Remove-Item $xfile
}
write-host "Getting _f details"
get-childitem \\server\F$ -recurse |select directory, name, length|exportcsv $xfile -append -noclobber -notypeinformation
write-host "Getting _H details"
get-childitem \\server\H$ -recurse |select directory, name, length|exportcsv $wfile -append -noclobber -notypeinformation
}
TIA
Andy
No there isn't a straightforward way to pass the function to the remote computers, well other than what you are already doing. :-) However you can put all that script in a file (dirsize.ps1) and pass that to Invoke-Command using the FilePath parameter:
Invoke-Command –ComputerName server –FilePath .\dirsize.ps1
The file will be copied to the remote computers and executed.
try this:
#Local Function
function Get-Bits
{
Get-Service -Name BITS
}
Invoke-Command -ComputerName RemoteServer -ScriptBlock ${function:Get-Bits}
personally i put all my functions in a psm1 file in other words a module on a network share and import the module while in the remote session.

Powershell script works in ISE but not in console

I am writing a powershell script that will start a process, monitor a folder for file changes, and then backup that folder when files are added/changed.
If I run my script from within the Powershell ISE, it works fine, I can monitor the folder and it will properly save a backup as expected.
The problem is that I want to run a batch file that will run the powershell script. But whenever I run the script from a powershell console, or whenver I run my batch file which runs the script, it doesn't work any longer. The script runs and the events are registered. When I copy a file over to the watched folder though, I ONLY get the changed event and not the created event, and the doStuff function is no longer called. I'm not sure how to go about debugging this :/
Following is my script. I have removed parts that don't pertain to what is actualyl wrong currently, so some variables I'm using here you won't see declared but they are there. I get the write-host's to the console when the changed event happens but not the created event (though as stated eariler in the ISE, I get both events and everything works just fine)
#unregister events, in case they weren't unregistered properly before. Just error siliently if they don't exist
Unregister-Event ConsoleStopped -ErrorAction SilentlyContinue
Unregister-Event FileCreated -ErrorAction SilentlyContinue
Unregister-Event FileChanged -ErrorAction SilentlyContinue
Unregister-Event TimerTick -ErrorAction SilentlyContinue
#start the console process
Write-Host Starting console process...
$consoleProcess = Start-Process "$consoleExe" -PassThru
#register to listen for when the console stops
Register-ObjectEvent $consoleProcess Exited -SourceIdentifier ConsoleStopped -Action {
Write-Host Console stopped
#unregister events
Unregister-Event ConsoleStopped -ErrorAction SilentlyContinue
Unregister-Event FileCreated -ErrorAction SilentlyContinue
Unregister-Event FileChanged -ErrorAction SilentlyContinue
if(!$timer.Enabled) {
Unregister-Event TimerElapsed -ErrorAction SilentlyContinue
Remove-Item $timer
}
Remove-Item $fsw
Remove-Item $consoleProcess
}
#watch all files/folders
$filter = '*.*' # You can enter a wildcard filter here.
# In the following line, you can change 'IncludeSubdirectories to $true if required.
$fsw = New-Object IO.FileSystemWatcher $folder, $filter -Property #{IncludeSubdirectories = $true;NotifyFilter = [IO.NotifyFilters]'DirectoryName, FileName, LastWrite'}
#register for FileCreated event
Register-ObjectEvent $fsw Created -SourceIdentifier FileCreated -Action {
write-host Created event has occurred
doStuff($Event)
}
#register for FileChanged event
Register-ObjectEvent $fsw Changed -SourceIdentifier FileChanged -Action {
Write-Host Change event has occurred
doStuff($Event)
}
function doStuff($event)
{
write-host doStuff has been called
$name = $Event.SourceEventArgs.Name
$changeType = $Event.SourceEventArgs.ChangeType
$timeStamp = $Event.TimeGenerated
Write-Host "The file '$name' in '$folder' was $changeType at $timeStamp" -fore green
if(!$timer.Enabled) {
Write-Host Starting save timer
Register-ObjectEvent $timer Elapsed -SourceIdentifier TimerElapsed -Action $TimerAction
$timer.Start()
Out-File "$backupDir\backup.log" -Append -Force -InputObject "A request for a backup created at $timeStamp"
}
else {
Write-Host A backup has already been request
}
}
function backupSave ()
{
Write-Host Starting backup...
$timestamp = Get-Date -Format o | foreach { $_ -replace ":", "." }
Copy-Item $folder "$backupDir\backup_$timestamp" -Recurse -Force
}
Try moving your functions to the beginning of your script.
i.e. move function doStuff($event) and function backupSave () to the beginning of the script.
The problem may be because when you are calling the functions from the script they haven't been defined yet. It works in Powershell ISE because you ran the script several times and the functions were defined at one time or another.