My main PowerShell code runs a function that logs to the Windows eventlog. If the level is error it uses a separate event ID which then our monitoring will pick up that exact ID and run an action. However, if I want to specify in the parameter of the main script (not the function) that this time running it use a different Event ID so it will NOT action monitoring, I don't know where to even start on that.
Is there a way to provide a switch parameter in the main script like $NoAlert which then changes the Event ID in the function?
The function of logging lives in a PowerShell module I created. I am importing the module at the beginning of the script and then calling the function during the main script body.
Here is the function:
function WriteLog-SRTProd {
Param(
[string]$logT,
[Parameter(Mandatory=$true)][string]$level,
[String]$LogFileDirT = "\\ServerA\Logs"
)
$RSLogfileT = (Get-ChildItem -Path $LogFileDirT |
sort LastWriteTime |
select -Last 1).Name
## make sure a level is correctly selected (mandatory)
if ("Error","Info","Warn" -NotContains $Level) {
throw "$($Environment) is not a valid name! Please use 'Error', 'Warn', or 'Info'"
}
if ($Level -eq "Info") {
Add-Content -Path "$LogFileDirT\$RSLogFileT" -Value "$(Get-Date -format MM-dd-yyyy::HH:mm:ss) INFO $logT"
Write-EventLog -LogName Application -Source TEST_MAINT -EntryType Information -EventId 100 -Message $logT -Category 0
}
if ($Level -eq "Warn") {
Add-Content -Path "$LogFileDirT\$RSLogFileT" -Value "$(Get-Date -format MM-dd-yyyy::HH:mm:ss) WARN $logT"
Write-EventLog -LogName Application -Source TEST_MAINT -EntryType Warning -EventId 200 -Message $logT -Category 0
}
if ($Level -eq "Error") {
Add-Content -Path "$LogFileDirT\$RSLogFileT" -Value "$(Get-Date -format MM-dd-yyyy::HH:mm:ss) ERROR $logT"
Write-EventLog -LogName Application -Source TEST_MAINT -EntryType Error -EventId 300 -Message $logT -Category 0
}
}
I'd like to run my script like this. When the $NoAlert is passed, it will send that switch to the function. Is this possible? Can I just add the switch in both places and use an if statement in the function for when the NoAlert switch is used?
PS C:\> .\Maintenance.ps1 -NoAlert
Param(
[switch]$NoAlert
)
WriteLog-SRTProd -level Error -logT "Custom Error Message"
I have created own function for logging and stored/installed as module, below is the part of my log module :
you can customize the write statements and add your code for event log. I have added 'NoAction' enum member as per your requirements.
I have used one Enum to separate the log levels
Enum Severity
{
Error = 3
Warning = 4
Informational = 6
Debug = 7
Verbose = 8
NoAction = 0 # AS PER YOUR REQUIREMENTS
}
function Write-Log()
{
[cmdletbinding()]
param
(
[Parameter(Position=0,mandatory=$true)]
[Severity] $LogLevel,
[Parameter(Position=1,mandatory=$true)]
[String] $Message
)
$TimeStamp = "$(Get-Date -format HH:mm:ss)" ;
Switch($LogLevel)
{
([Severity]::Error.ToString())
{
Write-Error "`t$TimeStamp : $Message`n" -ErrorAction Stop
break;
}
([Severity]::Warning.ToString())
{
Write-Warning "`t$TimeStamp : $Message`n" -WarningAction Continue
break;
}
([Severity]::Informational.ToString())
{
Write-Information "INROMATION:`t$TimeStamp : $Message`n" -InformationAction Continue
break;
}
([Severity]::Verbose.ToString())
{
Write-Verbose "`t$TimeStamp : $Message`n"
break;
}
([Severity]::NoAction.ToString())
{
Write-Verbose "`t$TimeStamp : $Message`n"
break;
}
} # END OF SWITCH
} # END OF FUNCTION
Sample Call :
Write-Log -LogLevel ([Severity]::Informational) -Message "test log message using info level"
Output :
INROMATION: 09:40:15 : test log message using info level
I have decided to just add a new parameter to both function and main script named $NoAlert. I have added an If($NoAlert){WriteLog-SRPProd -NoAlert} to the main script (messy, but its what I needed done). then in the Function, If($NoAlert){EventID 111}. so basically I am using the switch in the main script that then calls the NoAlert switch in the function. This is all done with a few added If/Else statements.
Hopefully that makes sense. Like I said its not the best answer, but I wanted to get it done and still provide an answer here in this post.
Related
This question already has an answer here:
Powershell Join-Path showing 2 dirs in result instead of 1 - accidental script/function output
(1 answer)
Closed 1 year ago.
I wrote a very simple script to acquire a random free drive letter.
The function finds a random free letter , creates a new empty text file of that drive letter name eg. Q.txt
I then return the value as $new_letter but when it comes out of the function somehow newly file created path is a part of the variable C:\AppPack\Logs\Q.txt Q
Is it something New-Item messing up with my $new_letter variable ?
function get_drive_letter()
{
$letter_acquired = $false
Do
{
$new_letter = Get-ChildItem function:[h-z]: -Name | ForEach-Object { if (!(Test-Path $_)){$_} } | random -Count 1 | ForEach-Object {$_ -replace ':$', ''}
write-host ("RIGHT AFTER " + $new_letter)
if (!(test-path "C:\AppPack\Logs\$new_letter.txt"))
{
New-Item -Path C:\AppPack\Logs\ -Name "$new_letter.txt" -ItemType "file"
write-host ("FROM FUNCTION " + $new_letter)
$letter_acquired = $true
return $new_letter
}
else
{
write-host ("LETTER USED ALREADY")
write-host ($new_letter)
}
}
while($letter_acquired = $false)
}
$drive_letter = $null
$drive_letter = get_drive_letter
write-host ("RIGHT AFTER FUNCTION " + $drive_letter)
OUTPUT :
RIGHT AFTER Q
FROM FUNCTION Q
RIGHT AFTER FUNCTION C:\AppPack\Logs\Q.txt Q
A PowerShell function outputs everything, not just the result of the expression right after return!
The additional file path you see is the output from New-Item ... - it returns a FileInfo object for the file you just created.
You can suppress output by assigning it to the special $null variable:
# Output from New-Item will no longer "bubble up" to the caller
$null = New-Item -Path C:\AppPack\Logs\ -Name "$new_letter.txt" -ItemType "file"
return $new_letter
Or by piping to Out-Null:
New-Item ... |Out-Null
Or by casting the entire pipeline to [void]:
[void](New-Item ...)
Although I recommend explicitly handling unwanted output at the call site, you can also work around this behavior with a hoisting trick.
To demonstrate, consider this dummy function - let's say we "inherit" it from a colleague who didn't always write the most intuitive code:
function Get-RandomSquare {
"unwanted noise"
$randomValue = 1..100 |Get-Random
"more noise"
$square = $randomValue * $randomValue
return $square
}
The function above will output 3 objects - the two garbage strings one-by-one, followed by the result that we're actually interested in:
PS ~> $result = Get-RandomSquare
PS ~> $result
unwanted noise
more noise
6400
Let's say we've been told to make as few modifications as possible, but we really need to suppress the garbage output.
To do so, nest the entire function body in a new scriptblock literal, and then invoke the whole block using the dot-source operator (.) - this forces PowerShell to execute it in the function's local scope, meaning any variable assignments persist:
function Get-RandomSquare {
# suppress all pipeline output
$null = . {
"unwanted noise"
$randomValue = 1..100 |Get-Random
"more noise"
$square = $randomValue
return $square
}
# variables assigned in the block are still available
return $square
}
I need to restart a service in a powershell script. The problem is that this service is a bit buggy and frequently needs to be shut down several times before it gets into the "stopped" state. Because of that I can't seem to use the Restart-Service cmdlet, instead I need to retry the Stop-Service cmdlet a few times. The same applies to starting the service.
So I figure this is a good place to write a function that will take an action (start or stop) and retry it a few times until it works. The problem is I'm not sure how to pass the action in as a parameter. I could just have the action be a String and then say if action == "start" do starcAction, but that won't be very clean. Is there any way I could pass a cmdlet like Stop-Service in as a parameter?
For the scenario you described you'd normally do something like this:
$maxTries = 5
switch ($args(0)) {
'start' {
Start-Service 'MySvc'
}
'stop' {
$i = 0
do {
Stop-Service 'MySvc'
$i++
} until ((Get-Service 'MySvc').Status -eq 'Stopped' -or $i -ge $maxTries)
if ((Get-Service 'MySvc').Status -ne 'Stopped') {
Write-Error "Cannot stop service."
exit 1
}
}
default {
Write-Error "Unknown action: $_"
exit 1
}
}
If you really want to avoid string arguments, you could use parameter sets like this:
[CmdletBinding(DefaultParameterSetName='none')]
Param(
[Parameter(Mandatory=$true,ParameterSetName='start')]
[Switch][bool]$Start = $false,
[Parameter(Mandatory=$true,ParameterSetName='stop')]
[Switch][bool]$Stop = $false
)
$maxTries = 5
switch ($PSCmdlet.ParameterSetName) {
'start' {
Start-Service 'MySvc'
}
'stop' {
$i = 0
do {
Stop-Service 'MySvc'
$i++
} until ((Get-Service 'MySvc').Status -eq 'Stopped' -or $i -ge $maxTries)
if ((Get-Service 'MySvc').Status -ne 'Stopped') {
Write-Error "Cannot stop service."
exit 1
}
}
'none' {
Write-Error "Usage: $($MyInvocation.MyCommand.Name) {-Start|-Stop}"
exit 1
}
}
Param([Parameter(Mandatory)] [ValidateSet('Start','Stop')] [string] $Action)
This allows the user to press Tab to select the possible values and will automatically reject all invalid input.
Passing in a well defined parameter (doesn't matter if it's a string or not) is actually cleaner than "passing in a commandlet" would be if there was such a thing.
Here is my script which returns a boolean
param($fileName, $path, $contextMenuItem, $automationDLLPath)
function CloseWindowsExplorer()
{
(New-Object -comObject Shell.Application).Windows() | foreach-object {$_.quit()}
}
Try
{
Import-Module $automationDLLPath
# Open the explorer window in a maximized form
Start-Process explorer $path -WindowStyle Maximized
Start-Sleep 1
Get-UIAActiveWindow
# Get the "Items View" in Explorer to go through all the lements
$list = Get-UIAList -Name 'Items View' -TimeOut 30000;
# Get the file specified in the feature file from the Items View
# Added a sleep because the VM takes time to perform the functions
Start-Sleep 1
$file = $list | Get-UIAListItem -Name $fileName;
# Perform a single click on the file to invoke a right click on it
Invoke-UIAListItemSelectItem -InputObject $file -ItemName $fileName;
# Added a sleep because the VM takes time to perform the functions
Start-Sleep 1
# Invoke the right click on the selected file
$menu = Invoke-UIAControlContextMenu -InputObject $file;
Start-Sleep 1
# select our context menu item
$menuItem = Get-UIAMenuItem -InputObject $menu $contextMenuItem -TimeOut 30000;
# Display error if the required item in the context menu is not found
if( $null -eq $menuItem){
%{ Write-Host 'cannot find menuItem' }
}
# Invoke the item if found in the context menu
else{
Invoke-UIAMenuItemClick -InputObject $menuItem
}
# close the windows explorer and return true
CloseWindowsExplorer
Write-Output "true"
}
Catch
{
# close the explorer window as a part of teardown and return false to reflect test failure
Write-Output "false"
CloseWindowsExplorer
}
I want the script to print the exact exception that was caught as well as return a boolean but in this case it is just returning false when the script fails. Any help is appreciated
Basically I need to print the exception as if the try catch block does not exist.
You need to use the special variable $_
This small example shows how it works:
try {
testmenow
} catch {
Write-Host $_
}
$_ is an object so you can do
$_|gm
in the catch block in order to see the methods you can call.
Why is the following code not working? According to this article the usage of global should be correct: http://technet.microsoft.com/en-us/library/ff730957.aspx
Function global:writeLog {
param($logType, $logString, $logFile)
$fileStream = New-Object IO.FileStream $logFile ,'Append','Write','Read'
$streamWriter = New-Object System.IO.StreamWriter $fileStream
$time = get-date -Format "hh:mm:ss"
$streamWriter.writeLine("[${time}][$logType] ${logString}")
$streamWriter.close()
}
$temp = {
writeLog -logType "INFO" -logString "Test" -logFile "d:\scripts\powershell\logtest.txt"
}
Start-Job -ScriptBlock $temp
get-job | receive-job -AutoRemoveJob -Wait
This is the exception that powershell throws
The term 'writeLog' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included,
verify that the path is correct and try again.
+ CategoryInfo : ObjectNotFound: (writeLog:String) [], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException
+ PSComputerName : localhost
From the documentation of Start-Job:
A Windows PowerShell background job runs a command "in the background" without interacting with the current session.
Therefor, the current session scope is ignored.
Trivial Solution: Define the function inside the scriptblock.
$JobScript = {
function write-log {
....
}
write-log <parameters>
}
Alternatively, check these related questions:
Powershell: passing parameters to a job
Variables in Start-Job
PowerShell jobs actually run in a separate PowerShell process. You can see this like so:
$pid
Start-Job {$pid} | Receive-Job -Wait
Where $pid is the current PowerShell's process id.
Anything that needs to be accessed from the script that runs in the job, must be either defined in the scriptblock passed to Start-Job i.e. function defined in the script block or as parameters passed into the script block using the -ArgumentList parameter on Start-Job or the script can dot source another script (or import a module) that contains the functions it needs. Personally, I would put shared functions in a module like Utils.psm1 and then import like so:
Start-Job {param($scriptdir) Import-Module $scriptdir\Utils.psm1; ...} -Arg $PSScriptRoot
Define the function in a script block, then use
Invoke-Command with NoNewScope to get it in the current scope
The InitializationScript parameter to get it into the job
#Create Shared Functions Script Block
[scriptblock] $func = {function getPID() {write-output "Function Running in process id: $pid!"}}
#Set up functions in normal script scope so they're accessible here
Invoke-Command -NoNewScope -ScriptBlock $func
write-output "Main script started"
#run the function from the parent script
getPID
#Create background job script
$jobScript = {getPID}
#Run background job
write-output "starting background job"
Start-Job $jobScript -name "Job1" -InitializationScript $func
get-job | Receive-Job
get-job | Stop-Job
get-job | Remove-Job
Here is an example:
function ChildF()
{
#Creating new function dynamically
$DynFEx =
#"
function DynF()
{
"Hello DynF"
}
"#
Invoke-Expression $DynFEx
#Calling in ChildF scope Works
DynF
}
ChildF
#Calling in parent scope doesn't. It doesn't exist here
DynF
I was wondering whether you could define DynF in such a way that it is "visible" outside of ChildF.
Another option would be to use the Set-Item -Path function:global:ChildFunction -Value {...}
Using Set-Item, you can pass either a string or a script block to value for the function's definition.
The other solutions are better answers to the specific question. That said, it's good to learn the most general way to create global variables:
# inner scope
Set-Variable -name DynFEx -value 'function DynF() {"Hello DynF"}' -scope global
# somewhere other scope
Invoke-Expression $dynfex
DynF
Read 'help about_Scopes' for tons more info.
You can scope the function with the global keyword:
function global:DynF {...}
A more correct and functional way to do this would be to return the function body as a script block and then recompose it.
function ChildF() {
function DynF() {
"Hello DynF"
}
return ${function:DynF}
}
$DynFEx = ChildF
Invoke-Expression -Command "function DynF { $DynFEx }"
DynF
Thanks to Richard's post. Kept having issues doing this simple thing. I revised for passing a function from local to remote.
#Method 1 Load the function from disk
$getCert = gc 'C:\MyScripts\getCert.ps1'
Invoke-Command $RemoteSrv -ScriptBlock {Set-Variable -name DefFN -value ($Args -join "`n") -scope global ; Invoke-Expression $DefFn } -ArgumentList $getCert
#Method 2 Load the function from local definition of function
Invoke-Command $RemoteSrv -ScriptBlock {Set-Variable -name DefFN -value ($Args -join "`n") -scope global ; Invoke-Expression $DefFn } -ArgumentList ('Function GetCert {'+(Get-Command GetCert).Definition+'}')
#Remote server now has function
Invoke-Command $RemoteSrv -ScriptBlock {getcert stackoverflow.com}
URL : stackoverflow.com
Expires : 12/14/2021 8:07:08 AM
SAN : DNS Name=*.askubuntu.com, DNS Name=.....
Thumbprint : ec0055be478411bafe98d11d63a5c9279ff0e173
IP : 151.101.193.69
Handle : 2866249748176
Issuer : CN=R3, O=Let's Encrypt, C=US
Subject : CN=*.stackexchange.com