Update 1:
Originally, I posted this with the title: "Scripts ignoring error handling in PowerShell module" as that is the current issue, however it seems more of a module issue, so I have renamed the title.
Update 2:
After a comment that made me question Azure cmdlets, I've tested with the most basic of scripts (added to the module) and the findings are the same, in that the error is not passed to the calling script, however, adding -errorVariable to Get-Service does return something (other than WriteErrorException) that I could probably harness in the handling of the error:
function Test-MyError($Variable)
{
Try
{
Get-Service -Name $variable -ErrorAction Stop -ErrorVariable bar
#Get-AzureRmSubscription -SubscriptionName $variable -ErrorAction Stop
}
Catch
{
Write-Error $error[0]
$bar
}
}
returns:
Test-MyError "Foo"
Test-MyError : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:3 char:1
+ Test-MyError "Foo"
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Test-MyError
The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot find any service with service name 'foo'.
However, if I run "Test-MyError" in ISE, then call the function, I get:
Test-MyError "Foo"
Test-MyError : Cannot find any service with service name 'Foo'.
At line:3 char:1
+ Test-MyError "Foo"
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Test-MyError
The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: Cannot find any service with service name 'Foo'.
So I am not sure what is happening when running "Test-MyError" in ISE and calling it, against it being dot-sourced in the PSM1 file and then calling it?
Do I now have to use -ErrorVariable and handle on that?
Original Question:
I have two functions in a module: Get-Subscription and Get-AllSubscriptions. Each function sits in its own PS1 file and the PSM1 file dot-sources them. The module seems fine as the module scripts are accessible using intelisense and the module loads without issue. I've used this structure in many modules and I haven't come across this problem before. (Although I wonder if MS have changed the way modules work in PS 5.1 as I have noticed using FunctionsToExport='x','y','Z' and Export-ModuleMember don't seem to behave the same way as they used to.)
Get-AllSubscriptions calls Get-Subscription.
If I am not logged into Azure, Get-Subscription should throw an error which is handled, prompting me to log in. This works as expected, if I call Get-Subscription from the Get-Subscription.ps1.
However, when I call Get-Subscription from the a new PS1 file, Get-AllSubscriptions or from the powershell console, it doesn't work. It iterates all the way through the do..until loop, without "handling" the errors as I would expect. On each iteration, it seems to throw a generic error:
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
However, I do see the last error, Get-Subscription : Unable to find requested subscription after 3 login attempts.
If I execute Get-Subscription in ISE, then call Get-Subscription in a new PS1 file or from Get-AllSubscriptions, it works as expected, however, once I re-import the module (Import-Module AzureVnetTools -Force -Verbose), it goes back to the incorrect behaviour.
If I dot-source Get-Subscription, inside the caller script, it works, but why? This is what should happen with the module's PSM1.
Can anyone help me work out what I am doing wrong here?
(PS 5.1, Windows 7)
Get-Subscription:
function Get-Subscription
{
[cmdletbinding()]
Param
(
[string]$SubscriptionName,
[string]$UserName,
[string]$code
)
$c=1
Write-Verbose "Checking access to '$SubscriptionName' with user '$UserName'..."
Do
{
Write-Verbose "Attempt $c"
Try
{
$oSubscription = Get-AzureRmSubscription -SubscriptionName $SubscriptionName -ErrorAction Stop -WarningAction SilentlyContinue
Write-Verbose "Subscription found: $($oSubscription.SubscriptionName)."
}
Catch
{
if($error[0].Exception.Message -like "*Please verify that the subscription exists in this tenant*")
{
Write-Verbose "Cannot find subscription '$SubscriptionName' with provided credentials."
$account = Login-AzureRmAccount -Credential (Get-Credential -UserName $Username -Message "Subscription '$SubscriptionName' user' password:")
}
elseif($error[0].Exception.Message -like "*Run Login-AzureRmAccount to login*")
{
Write-Verbose "No logged in session found. Please log in."
$account = Login-AzureRmAccount -Credential (Get-Credential -UserName $Username -Message "Subscription '$SubscriptionName' user' password:")
}
else
{
Write-Error $error[0]
}
}
$c++
}
until(($oSubscription) -or ($c -eq 4))
if($c -eq 4)
{
Write-Error "Unable to find requested subscription after $($c-1) login attempts."
break
}
$oSubscription | Add-Member -MemberType NoteProperty -Name Code -Value $code
$oSubscription
}
Get-AllSubscriptions:
function Get-AllSubscriptions
{
[cmdletbinding()]
param
(
[string]$MasterSubscription,
[string]$MasterSubscriptionCode,
[string]$MasterSubscriptionUsername,
[string]$ChildSubscription,
[string]$ChildSubscriptionCode,
[string]$ChildSubscriptionUsername
)
Write-Verbose "Getting all subscriptions..."
$oAllSubscriptions = #()
$oMasterSubscription = Get-Subscription -SubscriptionName $MasterSubscription -UserName $MasterSubscriptionUsername -code $MasterSubscriptionCode -Verbose
$oChildSubscription = Get-Subscription -SubscriptionName $ChildSubscription -UserName $ChildSubscriptionUsername -code $ChildSubscriptionCode -Verbose
$oAllSubscriptions = ($oMasterSubscription,$oChildSubscription)
$oAllSubscriptions
}
Test:
$splat2 = #{
SubscriptionName = "SomeSubscription"
Code = "S02"
Username = "some.user#somewhere.com"
}
#Write-Output "Dot-source:"
#. "D:\Temp\PS.Modules\AzureVnetTools\functions\public\Get-Subscription.ps1"
Get-Subscription #splat2 -verbose
Output:
Get-Subscription #splat2 -verbose
VERBOSE: Checking access to 'SomeSubscription' with user 'some.user#somewhere.com'...
VERBOSE: Attempt 1
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
VERBOSE: Attempt 2
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
VERBOSE: Attempt 3
Get-Subscription : Exception of type 'Microsoft.PowerShell.Commands.WriteErrorException' was thrown.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
Get-Subscription : Unable to find requested subscription after 3 login attempts.
At line:7 char:1
+ Get-Subscription #splat2 -verbose
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-Subscription
AzureVnetTools.psm1
#Get public and private function definition files.
$Public = #( Get-ChildItem -Path $PSScriptRoot\Functions\Public\*.ps1 -ErrorAction SilentlyContinue )
$Private = #( Get-ChildItem -Path $PSScriptRoot\Functions\Private\*.ps1 -ErrorAction SilentlyContinue )
#Dot source the files
Foreach($import in #($Public + $Private))
{
#write-error $import.fullname
Try
{
#Write-Host "Dot-sourcing file: $($import.fullname)."
. $import.fullname
}
Catch
{
Write-Error -Message "Failed to import function $($import.fullname): $_"
}
}
Export-ModuleMember -Function $Public.Basename
AzureVnetTools.psd1 (Relevant section):
FunctionsToExport = '*'
-ErrorAction Stop -WarningAction SilentlyContinue
Is it a warning that's being thrown instead of an error?
So my specific problem was that I was relying on handling the error and doing something based on that. The problem was caused by the way PowerShell's Write-Error works (or not) as I learned from the reply here, given by #Alek.
It simply wasn't passing the actual error back to the calling script. As #Alex suggested, I replaced Write-Error with $PSCmdlet.WriteError(). Although this didn't totally work.
In the Catch{} block, I then changed $error[0] to $_ and the full error was returned to the calling script / function.
I went one further and wrote a reusable function, added to my module:
function Write-PsError
{
[cmdletbinding()]
Param
(
[Exception]$Message,
[Management.Automation.ErrorCategory]$ErrorCategory = "NotSpecified"
)
$arguments = #(
$Message
$null #errorid
[Management.Automation.ErrorCategory]::$ErrorCategory
$null
)
$ErrorRecord = New-Object -TypeName "Management.Automation.ErrorRecord" -ArgumentList $arguments
$PSCmdlet.WriteError($ErrorRecord)
}
Which seems to be working well at the moment. I especially like the way intellisense picks up all the ErrorCategories. Not sure what or how ISE (PS 5.1 / Win 7) does that. I thought I was going to have to add my own dynamic parameter.
HTH.
Below is a simple script block, the script block works. However, I would like to suppress any errors that the script block would generate.
$Name = 'TEST'
$SB = { param ($DSNName) ;
$conn = new-object system.data.odbc.odbcconnection
$conn.ConnectionString = ('DSN='+ $DSNName)
$conn.open()
$ConState = $conn.State
$conn.Close()
$ConState
}
$test = Start-job -scriptblock $SB -args $Name -RunAs32 -ErrorAction Stop | wait-job | receive-job
What I am trying to get out of this is a simple test for a 32bit ODBC connection. If the connection fails the connection state will remain closed but I also get an exception error I would like to suppress
Exception calling "Open" with "0" argument(s): "ERROR [IM002] [Microsoft][ODBC Driver Manager] Data source name not found and no default driver specified"
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : OdbcException
+ PSComputerName : localhost
If I pipe to out-null my $test variable is empty. When I use a valid DSN Name everything works as desired.
You could use try..catch:
try {
$test = Start-job -scriptblock $SB -args $Name -RunAs32 -ErrorAction Stop | wait-job | receive-job
catch [System.Management.Automation.MethodInvocationException] {
# Do nothing here if you want to suppress the exception completely.
# Although redirecting it to a log file may be a better idea, e.g.
# $Error[0] | Out-File -FilePath "script.log"
}
I am trying to individually monitor memory usage of a process (w3wp.exe) that has multiple instances of itself by filtering out a string found in the process' CommandLine property.
It works when I run this script locally:
$proc = (WmiObject Win32_Process -Filter "Name = 'w3wp.exe'" | Where-Object {$_.CommandLine -like "*SomeTextFromCl*"})
$id = $proc.ProcessId
$ws = [math]::round((Get-Process -Id $id).WS/1MB)
Write-Host $ws
However, when I try to run it remotely through Invoke-Command, I get an error telling that the Id property's value is null:
Cannot bind argument to parameter 'Id' because it is null.
+ CategoryInfo : InvalidData: (:) [Get-Process], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.GetProcessCommand
+ PSComputerName : RemoteServerName
My Invoke-Command syntax is:
Invoke-Command -ComputerName RemoteServerName -FilePath script.ps1 -Credential $mycredential
I'm sure it's simple but I'm back to PS after a long absence and I had a look around but couldn't find anything really helpful.
You are writing the answer to the console. You use the ps1 as a function, so you should use:
return $ws
instead of
write-host $ws
I'm trying to execute the following two lines of code in PowerShell v2.0 as a job, and am having trouble. I think I have the syntax right, but I can't get it to do what I think it should do, so I clearly am doing something wrong...
$report = <command line thing>
invoke-expression $report
These two lines work in PowerShell. But when I try to put it into a start-job command:
start-job -scriptblock {invoke-expression $report} -argumentlist $report | wait-job | receive-job
I get the following error:
Cannot bind argument to parameter 'Command' because it is null.
+ CategoryInfo : InvalidData: (:) [Invoke-Expression], ParameterBindingValidationException
+ FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.InvokeExpre
ssionCommand
+ PSComputerName : localhost
I understand that error as saying that the argument I'm passing the invoke-expression is null, but, I'd thought that by passing $report in the -argumentlist param, it'd get through?
You cannot access $report directly unless you do:
-scriptblock {param($report) invoke-expression $report}
The param($report) part captures the $report variable passed in via -ArgumentList and makes it available for use inside the scriptblock.
I need to get some procs by the owner. My demo script below will first look for procs by owner locally, then it will do the same thing, but it invokes the command on the same box:
cls
write-host 'LOCAL CALL: '
$procs = #(Get-WmiObject win32_process |? {($_.getowner().user -eq 'APP_ACCOUNT') })
write-host $procs.count
$func = {
$procs = #(Get-WmiObject win32_process |? {($_.getowner().user -eq 'APP_ACCOUNT') })
write-host $procs.count
}
write-host 'REMOTE CALL: '
$session = New-PSSession -ComputerName 'SERVER'
$job = Invoke-Command -Session $session -ScriptBlock $func -AsJob
Wait-Job -Job $job
$job | Receive-Job
$job | Remove-Job
Remove-PSSession -Session $session
Most of the time when I run my script it errors with the following output:
LOCAL CALL:
38
REMOTE CALL:
Id Name PSJobTypeName State HasMoreData Location Command
-- ---- ------------- ----- ----------- -------- -------
26 Job26 RemoteJob Completed True SERVER ...
Exception calling "GetOwner" : "Not found "
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : WMIMethodException
+ PSComputerName : SERVER
38
So that first 38 is the number of procs it found for the owner running locally. It finds 38 the second time as well, but errors calling getowner. I don't understand why since it worked the first time. Is it operating in some kind of "bubble" when I invoke the command? In my larger script this is causing me more severe issues as the job state goes to failed and execution halts even though it is throwing the same error. One problem at a time though.
Seems I needed to do a better job of making sure my processes still exist before filtering by owner:
$procs = #()
$allProcs = #(Get-WmiObject win32_process)
foreach($proc in $allProcs)
{
$procActive = get-process -Id $proc.processId -ErrorAction SilentlyContinue
if($procActive)
{
if($proc.getowner().user -eq 'jdholbrook')
{
$procs += $proc
}
}
}
write-host $procs.count
This is probably because the process for which you want to query the owner doesn’t exist anymore.
You can simulate this behaviour on your local PC as follows:
Start some application, like notepad.exe for example. Now run:
$w = (Get-WmiObject win32_process) # Your notepad process will now be the last in the `$w` array.
Close the notepad.exe process.
Now pipe the contents of $w to get the owners:
$w | % {$_.getowner()}
For the last object you will get:
Exception calling "GetOwner" : "Not found "
At line:1 char:20
+ $w | % {$_.getowner <<<< ()}
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : WMIMethodException
To make sure this is the notepad.exe you just closed you can double-check:
$w[-1]; # last object
$w[-1].getowner(); # error
So, now you know what is causing, you can start thinking about how to handle it...