Powershell module abort loading on unsupported hosts - powershell

Glossary:
Host: PowershellHost session
Interactive: [Environment]::UserInteractive -eq $True
Scenario:
Create a powershell module that only will abort propertly and without error on failed condition. In this case, some commands/modules only work properly in full interactive hosts, like ISE and Console, but not in fake interactive hosts like NuGet Package Manager Console
Failed Solution:
# Add value to Powershell manifest(psd1)
# Issue: Only supports a string for the `PowerShellHostName` property. How to specify both `ConsoleHost` and `Windows PowerShell ISE Host`? Unknown if this property supports regex, and even if it does, will the behavior change since it's not documented?
#{
....
# Name of the Windows PowerShell host required by this module
# PowerShellHostName = ''
....
}
Failed Solution:
# Check for interactive shell
# Issue: UserInteractive is still set in embedded shells like NuGet package manager
# console. Commands that expect user input directly often hang.
if([Environment]::UserInteractive) {
# Do stuff, dotsource, etc
}
Failed Solution:
# Issue: returning still leaves module loaded, and it appears in Get-Module list
# Even if value is supplied for return, powershell's return statement is 'special'
# and the value is ignored
if($Host.Name -inotmatch '(ConsoleHost|Windows PowerShell ISE Host)') {
Write-Warning "Host [$($Host.Name)] not supported, aborting"
return
}
Failed Solution:
# Issue: Module isn't loaded, so it can't be unloaded
if( $Host.Name -inotmatch '(ConsoleHost|Windows PowerShell ISE Host)' ) {
Remove-Module ThisModuleName
}
Failed Solution:
# Issue: Powershell module error output is just passthrough, import-module
# still reports success, even though $Error is has more stuff than before
if( $Host.Name -inotmatch '(ConsoleHost|Windows PowerShell ISE Host)' ) {
Write-Error "Unsupported Host:" $Host.Name
}
Annoying solution:
# Issue: Leaves two errors on the stack, one for the throw, one for the module not
# loading successfully
if($Host.Name -inotmatch '(ConsoleHost|Windows PowerShell ISE Host)') {
throw "Host [$($Host.Name)] not supported, aborting"
}
Not a solution:
Force user to wrap the import every time.
Questionable Solution:
Split module into nested submodules, one for 'Common', and one for each supported Host. Use subfolder for each, and duplicate psd1 for each. Looks like it will end up being a maintainability nightmare, especially with respect to nested dependencies.
UberModule
/ModuleCommon
/ModuleCommon.(psd1|psm1)
/ConsoleHostSpecific
/ConsoleHostSpecific.(psd1|psm1)
/IseHostSpecific
/IseHostSpecific.(psd1|psm1)
/etc...
Is there a better way to do this, or is the uber-module split the only way to go?

Take a look at the #requires keyword, it may offer a few options that you have not tried yet. I don't know if NuGet Package Manager Console has a unique ShellId or not.
#Requires –ShellId Microsoft.PowerShell
http://technet.microsoft.com/en-us/library/hh847765.aspx

Related

Faulty PowerShell cmdlets filling up $Error automatic variable

In order to be informed when PowerShell Startup / Logon scripts running on remote computers have bugs, I tend to end scripts with the following:
If ($Error) {
(Code that sends a notification email to system administrators attaching the contents of the $Error variable for troubleshooting)
}
This is a great 'tell tale' to pick up edge cases / bugs. However, I've found some basic built-in PowerShell cmdlets dump data into $Error even on successful runs - for example, try:
$Error.Clear()
Get-NetIPConfiguration
$Error
And you'll see a load of errors in $Error that are not shown during normal output but look like:
Get-NetRoute : No matching MSFT_NetRoute objects found by CIM query for instances of the ROOT/StandardCimv2/MSFT_NetRoute class on the CIM server: SELECT * FROM
MSFT_NetRoute WHERE ((DestinationPrefix LIKE '0.0.0.0/0')) AND ((InterfaceAlias LIKE 'OpenVPN Wintun')). Verify query parameters and retry.
Get-NetConnectionProfile : No MSFT_NetConnectionProfile objects found with property 'InterfaceAlias' equal to 'Local Area Connection'. Verify the value of the property and
retry.
or
$Error.Clear()
Get-NetIPAddress
$Error
will return:
“Infinite : The term '“Infinite' 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.
(A nice little bug for Microsoft to solve at some point, doubtless!)
Since it's unlikely that the cmdlets will be fixed any time soon, is there a way to run these cmdlets without them clogging up $Error with their useless information?
This is not a duplicate of Powershell: How can I stop errors from being displayed in a script? since that covers errors that actually display in red on the PowerShell console during a 'failed' run of the cmdlet; this is about errors generated by some cmdlets in the background during an apparently 'successful' run of a cmdlet which for some reason only get written to the automatic $Error variable.
Nonetheless I have already tried a number of solutions suggested in that post:
Running the cmdlets with -ErrorAction Ignore
Running the cmdlets with -ErrorAction SilentlyContinue
Running the cmdlets inside try {} catch {}
Running the cmdlets inside try {} catch {} with -ErrorAction Stop
Running the cmdlets with 2>$null following them
Setting $ErrorActionPreference = "SilentlyContinue" before running the cmdlets
I may be asking the impossible, but the way these cmdlets behave does make $Error very hard to use as an actual log, just want to know if I'm missing a trick.
I would like to be able to encapsulate buggy cmdlets in such a way that 'hidden' errors do not go into the automatic $Error variable.
I agree with #zett42' comment: I think you can't really prevent cmdlets from adding to $Error.
Also knowing that these "phantom errors" might already occur with a simple (Try/Catch) statement like:
Try { 1/0 } Catch {}
Anyways, you might consider to mark the last one and remove the errors added after that/ Like:
$HashCode = if ($Error) { $Error[0].GetHashCode() }
Get-NetIPAddress
While($Error -and $Error[0].GetHashCode() -ne $HashCode) { $Error.RemoveAt(0) }
Use the common -ErrorVariable parameter in order to collect only the (non-terminating) errors directly emitted or intentionally passed through by a cmdlet (those that it internally silences or ignores will not be captured):
# $errs is a self-chosen variable; note that it must be specified WITHOUT $
Get-NetIPAddress -ErrorVariable errs
# $errs now contains any (non-terminating) errors emitted by the
# Get-NetIPAddress call, as a [System.Collections.ArrayList] instance.
# (If no errors occurred, the list is empty).
Note: To also silence errors, combine -ErrorVariable errs with -ErrorAction SilentlyContinue (-ErrorAction SilentlyContinue does not work - see below).
The automatic $Error variable is designed to provide a session-wide log of all errors.
However, (script) cmdlets that deliberately ignore errors can avoid logging unnecessary errors by using -ErrorAction Ignore in internal calls - assuming that the errors are not only to be silenced, but also needn't be inspected.
(If errors need to be inspected after having collected them with -ErrorVariable, use of -ErrorAction Ignore is not an option, because it prevents error collection.)
The CDXML-based cmdlets from the NetTCPIP module, such as Get-NetIPAddress unfortunately use -ErrorAction SilentlyContinue in cases where -ErrorAction Ignore would suffice.
Conceivably, the cmdlet-generation code predates v3 of PowerShell, when the Ignore value was introduced.

How can I change the default PowerShell error for when a command needs elevation?

I have a lot of code in a PowerShell script that are mix of commands that need elevation to run and commands that don't, those that need elevation show errors in PowerShell console like:
"You don't have enough permissions to perform the requested operation"
and
"Requested registry access is not allowed."
is there a way to globally suppress only the kinds of errors that PowerShell shows due to lack of necessary privileges?
I thought about a function that checks for elevation and performs actions based on the result, like this:
https://devblogs.microsoft.com/scripting/use-function-to-determine-elevation-of-powershell-console/
Function Test-IsAdmin
{
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal $identity
$principal.IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}
and
if(-NOT (Test-IsAdmin))
{ write-host "Skipping Admin command" }
else { $code }
but I don't know how to apply it globally to the whole script, so that commands that don't need elevation run, and those that need elevation show a custom message or skip that part silently.
another thing that can help my situation would be to find out if a PowerShell command needs elevation before actually running it and causing it to show errors in console due to lack of privileges.
It seems that errors stemming from a lack of privileges typically - but not necessarily - involve a System.UnauthorizedAccessException or System.Security.SecurityException .NET exception behind the scenes, whose name is then reflected as part of the .FullyQualifiedErrorId property of the resulting PowerShell error record, which is of type System.Management.Automation.ErrorRecord.
Assuming that this applies to all errors you care about, you can use a (rarely used anymore) trap statement as follows:
trap {
if ($_.FullyQualifiedErrorId -match 'UnauthorizedAccessException|SecurityException') {
Write-Warning "Skipping admin command ($($_.InvocationInfo.Line.Trim()))"
continue # Suppress the original error and continue.
}
# If the error was created with `throw`, emit the error and abort processing.
# SEE CAVEAT BELOW.
elseif ($_.Exception.WasThrownFromThrowStatement) { break }
# Otherwise: emit the error and continue.
}
# ... your script
Caveat:
If your script implicitly raises script-terminating errors - via -ErrorAction Stop or $ErrorActionPreference = 'Stop' - the above solution in effect turns them into statement-terminating errors and continues execution (only explicit script-terminating errors created with a throw statement are recognized as such in the code above, and result in the script getting aborted).
Unfortunately, as of PowerShell 7.2.x, there is no way to generally discover whether a given error is (a) non-terminating, (b) statement-terminating or (c) script-terminating (fatal).
See GitHub issue #4781 for a proposal to add properties to [System.Management.Automation.ErrorRecord] to allow such discovery in the future.

How to determine if Write-Host will work for the current host

Is there any sane, reliable contract that dictates whether Write-Host is supported in a given PowerShell host implementation, in a script that could be run against any reasonable host implementation?
(Assume that I understand the difference between Write-Host and Write-Output/Write-Verbose and that I definitely do want Write-Host semantics, if supported, for this specific human-readable text.)
I thought about trying to interrogate the $Host variable, or $Host.UI/$Host.UI.RawUI but the only pertinent differences I am spotting are:
in $Host.Name:
The Windows powershell.exe commandline has $Host.Name = 'ConsoleHost'
ISE has $Host.Name = 'Windows PowerShell ISE Host'
SQL Server Agent job steps have $Host.Name = 'Default Host'
I have none of the non-Windows versions installed, but I expect they are different
in $Host.UI.RawUI:
The Windows powershell.exe commandline returns values for all properties of $Host.UI.RawUI
ISE returns no value (or $null) for some properties of $Host.UI.RawUI, e.g. $Host.UI.RawUI.CursorSize
SQL Server Agent job steps return no values for all of $Host.UI.RawUI
Again, I can't check in any of the other platforms
Maintaining a list of $Host.Name values that support Write-Host seems like it would be bit of a burden, especially with PowerShell being cross-platform now. I would reasonably want the script to be able to be called from any host and just do the right thing.
Background
I have written a script that can be reasonably run from within the PowerShell command prompt, from within the ISE or from within a SQL Server Agent job. The output of this script is entirely textual, for human reading. When run from the command prompt or ISE, the output is colorized using Write-Host.
SQL Server jobs can be set up in two different ways, and both support capturing the output into the SQL Server Agent log viewer:
via a CmdExec step, which is simple command-line execution, where the Job Step command text is an executable and its arguments, so you invoke the powershell.exe executable. Captured output is the stdout/sterr of the process:
powershell.exe -Command x:\pathto\script.ps1 -Arg1 -Arg2 -Etc
via a PowerShell step, where the Job Step command text is raw PS script interpreted by its own embedded PowerShell host implementation. Captured output is whatever is written via Write-Output or Write-Error:
#whatever
Do-WhateverPowershellCommandYouWant
x:\pathto\script.ps1 -Arg1 -Arg2 -Etc
Due to some other foibles of the SQL Server host implementation, I find that you can emit output using either Write-Output or Write-Error, but not both. If the job step fails (i.e. if you throw or Write-Error 'foo' -EA 'Stop'), you only get the error stream in the log and, if it succeeds, you only get the output stream in the log.
Additionally, the embedded PS implementation does not support Write-Host. Up to at least SQL Server 2016, Write-Host throws a System.Management.Automation.Host.HostException with the message A command that prompts the user failed because the host program or the command type does not support user interaction.
To support all of my use-cases, so far, I took to using a custom function Write-Message which was essentially set up like (simplified):
$script:can_write_host = $true
$script:has_errors = $false
$script:message_stream = New-Object Text.StringBuilder
function Write-Message {
Param($message, [Switch]$iserror)
if ($script:can_write_host) {
$private:color = if ($iserror) { 'Red' } else { 'White' }
try { Write-Host $message -ForegroundColor $private:color }
catch [Management.Automation.Host.HostException] { $script:can_write_host = $false }
}
if (-not $script:can_write_host) {
$script:message_stream.AppendLine($message) | Out-Null
}
if ($iserror) { $script:has_errors = $true }
}
try {
<# MAIN SCRIPT BODY RUNS HERE #>
}
catch {
Write-Message -Message ("Unhandled error: " + ($_ | Format-List | Out-String)) -IsError
}
finally {
if (-not $script:can_write_host) {
if ($script:has_errors) { Write-Error ($script:message_stream.ToString()) -EA 'Stop' }
else { Write-Output ($script:message_stream.ToString()) }
}
}
As of SQL Server 2019 (perhaps earlier), it appears Write-Host no longer throws an exception in the embedded SQL Server Agent PS host, but is instead a no-op that emits nothing to either output or error streams. Since there is no exception, my script's Write-Message function can no longer reliably detect whether it should use Write-Host or StringBuilder.AppendLine.
The basic workaround for SQL Server Agent jobs is to use the more-mature CmdExec step type (where Write-Output and Write-Host both get captured as stdout), but I do prefer the PowerShell step type for (among other reasons) its ability to split the command reliably across multiple lines, so I am keen to see if there is a more-holistic, PowerShell-based approach to solve the problem of whether Write-Host does anything useful for the host I am in.
Just check if your host is UserInteractive or an service type environment.
$script:can_write_host = [Environment]::UserInteractive
Another way to track the output of a script in real time is to push that output to a log file and then monitor it in real time using trace32. This is just a workaround, but it might work out for you.
Add-Content -Path "C:\Users\username\Documents\PS_log.log" -Value $variablewithvalue

How does one test if a program/executable is present on a system in powershell?

I have a script that requires openssl be installed on the system. I want to do a check to see if it is installed. I've considered using test-path but because this script will be on many computers, there's no way of knowing where the user installed openssl or if it is in the system path.
Is there a way to do something like test-command openssl (I know that doesn't exist) and get an error level or the like to return in powershell?
Many thanks!
Use the Get-Command cmdlet to explicitly test if executables can be called by their name only (implying that they're in one of the directories listed in $env:PATH) and for command discovery in general:
$found = [bool] (Get-Command -ErrorAction Ignore -Type Application openssl)
Write-Verbose -vb "openssl.exe found? $found"
Casting to [bool] evaluates to $true only if Get-Command returns output, which it only does if the executable is found.
-Type Application ensures that only external programs are considered; by default, Get-Command finds commands of all types, including *.ps1 scripts, cmdlets, functions, and aliases.
The Get-Command by itself would also allow you to find out the executable's full path, via the output object's .Source property.
Alternatively, if all that matters is whether the executable is available, you can simply try to execute your actual command and handle an error indicating that the executable is not available via try / catch:
try {
openssl ... # your *actual* command, not a test
} catch [System.Management.Automation.CommandNotFoundException] {
# Handle the error, reflected in $_
Write-Warning $_
# ...
}
Note that you don't strictly need to catch [System.Management.Automation.CommandNotFoundException] specifically, if you're confident that the only error that can occur is due to a missing executable, but note that you could then get potential false positives from errors that occur during evaluation of expression-based arguments, such as attempting to pass (1 / 0) as an argument.
Also note that attempting to test the availability of an executable by invoking it without arguments inside a try / catch statement is not a viable approach, because the executable may enter an interactive shell (as is the case with openssl) or produce undesired side effects.
You can use try/catch blocks to do something like the following:
$OpenSSLMissing = $false
try {openssl} catch {$OpenSSLMissing = $true}
if ($OpenSSLMissing)
{
# Do something
}
else
{
# Do something else
}

How to perform Import-Module in powershell script used in File Server Resource Manager Classification script

I am trying to use the active directory PowerShell module inside a classification rule in File server resource manager on windows server 2012 R2.
When I try to just perform:
Import-Module ActiveDirectory
It will crash (I assume) and not update the classification property anymore.
I tried setting the script parameter -ExecutionPolicy Unrestricted, but that didn't help.
Anyone know how to get it to work?
non working code:
# Global variables available:
# $ModuleDefinition (IFsrmPipelineModuleDefinition)
# $Rule (IFsrmClassificationRule)
# $PropertyDefinition (IFsrmPropertyDefinition)
#
# And (optionally) any parameters you provide in the Script parameters box below,
# i.e. "$a = 1; $b = 2" . The string you enter is treated as a script and executed so the
# variables you define become globally available
# optional function to specify when the behavior of this script was last modified
# if it consumes additional files. emit one value of type DateTime
#
# function LastModified
# {
# }
# required function that outputs a value to be assigned to the specified property for each file classified
# emitting no value is allowed, which causes no value to be assigned for the property
# emitting more than one value will result in errors during classification
# begin and end are optional; process is required
#
function GetPropertyValueToApply
{
# this parameter is of type IFsrmPropertyBag
# it also has an additional method, GetStream, which returns a IO.Stream object to use for
# reading the contents of the file. Make sure to close the stream after you are done reading
# from the file
param
(
[Parameter(Position = 0)] $PropertyBag
)
process
{
Import-Module activedirectory
$users = Get-ADUser -filter * -Properties SID,Department
return "dummy result";
}
}
As note: This works perfectly fine in a PowerShell console; that isn't the issue. It's running the code as classifier for the file server resource manager.
Worked my way around it now by just creating a CSV file with the result of the Get-ADUser and loading that inside the script for now (so I don't require any non standard modules). But it would be nicer to just run this without a dependency on some external task.
The classification script is executed from a the File Server Resource Manager Service (not from the UI you are looking at), which is running under the system account.
So you either need to modify under which account the service is running or give the account rights to access the objects you require to access. In my case Active Directory.