1

Had to rewrite this post with the actual code since there must be some kind of difference with binary modules.

The full cmdlet is as follows (apologies for length):

In a nutshell, this function basically uses the SMO library and pulls information about a database users from a SQL Server Instance.

namespace Namespace
{
    using Microsoft.SqlServer.Management.Smo;
    using System;
    using System.Collections.Generic;
    using System.Management.Automation;

    using static PrivateFunctions;

    [Cmdlet(VerbsCommon.Get, "DatabaseUserInformation")]
    public class GetDatabaseUserInformationCmdlet : PSCmdlet
    {
        [Parameter(Mandatory = true,
                   HelpMessage = "The user name of the account to retrieve information for.",
                   Position = 0,
                   ValueFromPipeline = true,
                   ValueFromPipelineByPropertyName = true)]
        public string LogonName { get; set; }

        private List<object> userInformation = new List<object>();

        protected override void ProcessRecord()
        {
            string samAccountName = GetSamAccountName(LogonName);
            Login login = null;
            User user = null;

            WriteVerbose($"Getting login for account: {samAccountName}...");
            try
            {
                login = GetLogin(samAccountName);
            }
            catch (InvalidOperationException invalidOperation)
            {
                ThrowTerminatingError(new ErrorRecord(
                                      invalidOperation,
                                      "LoginNotFound",
                                      ErrorCategory.InvalidOperation,
                                      login));

            }

            WriteVerbose($"Getting user for login: {login.Name}...");
            try
            {
                user = GetUser(login);
            }
            catch (InvalidOperationException invalidOperation)
            {
                ThrowTerminatingError(new ErrorRecord(
                                      invalidOperation,
                                      "UserNotFound",
                                      ErrorCategory.InvalidOperation,
                                      user));
            }

            WriteVerbose($"Gathering information for user: {user.Name}");
            var information = new
            {
                LoginName = login.Name,
                UserName = user.Name,
                FullAccess = TestFullAccessOnDatabase(user),
                Roles = user.EnumRoles()
            };

            userInformation.Add(information);

        }

        protected override void EndProcessing()
        {
            WriteVerbose("Writing information to output.");
            userInformation.ForEach(item => WriteObject(item));
        }
    }
}

The cmdlet can be used with a single argument:

Get-DatabaseUserInformation user1

Or I also want to be able to pipe an array to it when dealing with multiple users.

@('user1', 'user2','user3') | Get-DatabaseUserInformation

If I'm using a single value and that user doesn't exist, then fair enough, it terminates, and it's a case of correcting it and running it again.

But when I'm using it with multiple values, if one of them doesn't exist, it doesn't give any output as all, only the exception.

So the output I get when everything is OK is something like this (with verbose on):

VERBOSE: Getting login for account: DOMAIN\user1...
VERBOSE: Getting user for login: DOMAIN\user1...
VERBOSE: Gathering information for user: dbo
VERBOSE: Getting login for account: DOMAIN\user2...
VERBOSE: Getting user for login: DOMAIN\user2...
VERBOSE: Gathering information for user: user2
VERBOSE: Getting login for account: DOMAIN\user3...
VERBOSE: Getting user for login: DOMAIN\user3...
VERBOSE: Gathering information for user: user3
VERBOSE: Writing information to output.

LoginName           UserName                     FullAccess Roles
---------           --------                     ---------- -----
DOMAIN\user1        dbo                          True       {db_owner}
DOMAIN\user2        user2                        False      {role1}
DOMAIN\user3        user3                        False      {}

What I get when something is wrong: (in this case, user2 has been misspelt.)

Get-DatabaseUserInformation : A login for the account 'DOMAIN\usr2' does not
exist in the database.
At line:1 char:6
+ $x | Get-DatabaseUserInformation -verbose
+      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Get-DatabaseUserInformation], InvalidOperationException
    + FullyQualifiedErrorId : LoginNotFound,DatabaseUserManagement.GetDatabase
   UserInformationCmdlet

What I want to happen is something similar to this.

VERBOSE: Getting login for account: DOMAIN\user1...
VERBOSE: Getting user for login: DOMAIN\user1...
VERBOSE: Gathering information for user: dbo
VERBOSE: Getting login for account: DOMAIN\usr2...
Get-DatabaseUserInformation : A login for the account 'DOMAIN\usr2' does not
exist in the database.
At line:1 char:6
+ $x | Get-DatabaseUserInformation -verbose
+      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidOperation: (:) [Get-DatabaseUserInformation], InvalidOperationException
    + FullyQualifiedErrorId : LoginNotFound,DatabaseUserManagement.GetDatabase
   UserInformationCmdlet
VERBOSE: Getting login for account: DOMAIN\user3...
VERBOSE: Getting user for login: DOMAIN\user3...
VERBOSE: Gathering information for user: user3
VERBOSE: Writing information to output.

LoginName           UserName                     FullAccess Roles
---------           --------                     ---------- -----
DOMAIN\user1        dbo                          True       {db_owner}
DOMAIN\user3        user3                        False      {}
Jake
  • 1,701
  • 3
  • 23
  • 44
  • When I run it I get 3, not 5 – Sean Oct 09 '15 at 15:02
  • Odd... It's skipping the first 4 elements in the array. I didn't expect that. – Jake Oct 09 '15 at 15:04
  • Also, which version of Powershell are you using? – Sean Oct 09 '15 at 15:13
  • PowerShell version is 2.0. But this should run on 3.0 and 4.0 as well. – Jake Oct 09 '15 at 15:15
  • Your function and function call are odd. Its called `Get-StringLength` but you are passing it an array? – Matt Oct 09 '15 at 15:28
  • @Matt, see the actual code from my program. The SSCCE wasn't performing as expected. – Jake Oct 09 '15 at 15:28
  • Well, you should definitely put your code into an explicit Process{} block. – EBGreen Oct 09 '15 at 15:32
  • I don't know c# but you made the error a terminating error with `ThrowTerminatingError` did you not? – Matt Oct 09 '15 at 15:40
  • @Matt. Yes. I assume in pure Powershell, when it encounters an exception/error, it terminates. Which is why in the old example I tried to get a non-existent property on an object. But when I was testing the old example, it just continued to process the remaining objects. – Jake Oct 09 '15 at 15:49
  • Powershell has a default behaviour for terminating errors but you can change that globally or use try/catch or set `-ErrorAction` on supported cmdlets. – Matt Oct 09 '15 at 15:51
  • @Matt, Setting erroraction didnt work in this case as per Sean's answer. I'll check what happens with try-catch – Jake Oct 09 '15 at 15:54
  • Those were meant in general... It would not matter if you still throw a terminating error in your catch block. Also not user how EA works in the pipe of the top of my head. Do you want a PowerSehll answer or C# answer for this? – Matt Oct 09 '15 at 15:55
  • I havent written any PSCmdlets in c# yet but what surprises me right away is that you want to pass in an array of string yet the input parameter is clearly a single value property, not an array or a list. – Santhos Oct 09 '15 at 16:00
  • @Matt, PowerShell. I'm just looking for a way to handle exceptions when processing multiple items. Even if it means having to rewrite my C# code. EOTD, I just want it to continue processing the next item/rest of the items if an exception gets thrown. – Jake Oct 09 '15 at 16:01
  • @Santhos. In my old version. LogonName was set to a `string[]`. But that didn't feel right for some reason. However I think that might be the way I'm supposed to write the cmdlet. I've never actually checked how it would behave if you passed a single object to a parameter expecting an array, – Jake Oct 09 '15 at 16:03
  • @Jake: Well you would have an array containing one object :) Did you want to structure the sentence vice versa? – Santhos Oct 09 '15 at 16:05
  • Probably. I'll check this again when I get home in about 20/30 minutes time. – Jake Oct 09 '15 at 16:06

5 Answers5

3

EDIT: The problem clearly is in the ThrowTerminating error. Read the msdn description here:

https://msdn.microsoft.com/en-us/library/system.management.automation.cmdlet.throwterminatingerror(v=vs.85).aspx

The remark section states that you should not use this approach if you want to continue processing the pipeline:

When a cmdlet encounters a terminating error, call this method rather than simply throwing an exception. Calling this method allows the cmdlet to attach additional error record information that describes the condition that caused the terminating error. When this method is called, the Windows PowerShell runtime catches the error record and then starts shutting down the pipeline. For more information about error reporting and error records, see Windows PowerShell Error Reporting. This method should not be used when errors occur where the cmdlet can continue processing records. To send error reports when nonterminating errors occur, call the WriteError method. For more information about cmdlets, see Windows PowerShell Cmdlets.


You have to write an advanced function like this:

Function Get-StringLength()
{
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $value
    )
    Begin {}
    Process {
        Write-Output $value.Length
    }
    End {}
}

The Begin block is where you prepare something, the Process block is where you process each object from the pipeline, that means it is run for each object in pipeline. You clean up the mess in the End block.

Because it is a cmdlet then you can use ErrorAction to control what happens on error:

$values | Get-StringLength -ErrorAction SilentlyContinue

You can even put those errors in a variable using:

$values | Get-StringLength -ErrorAction SilentlyContinue -ErrorVariable MyErrors
Santhos
  • 3,348
  • 5
  • 30
  • 48
  • This doesn't work in my actual code. It just throws an exception and exits with no output. – Jake Oct 09 '15 at 15:08
  • 2
    I have run it successfully in powershell ise – Santhos Oct 09 '15 at 15:12
  • Yes, your example works in PowerShell, but in my actual code it crashes. Looks like I'll have to anonymise and post 76 lines of code. – Jake Oct 09 '15 at 15:13
  • It depends what you want to do with it and what you expect the funciton returns. I have rewrote the Write-Output to clear the unnecessary parenthesis and checked the return type, it is int32 (not surprising). – Santhos Oct 09 '15 at 15:16
  • 1
    I think I may need to have another look at how these cmdlets are implemented. Probably going to have another question on this later, but I'll deal with this first. – Jake Oct 09 '15 at 19:15
1

Maybe throw in a try/catch statement, usually works for me. Otherwise, I would check out these two threads:

Continue execution on Exception

https://social.technet.microsoft.com/forums/scriptcenter/en-US/e9ee76cd-3446-4507-b9e7-60863550fa00/powershell-trycatch-not-continuing-after-error

Community
  • 1
  • 1
Nick
  • 1,178
  • 3
  • 24
  • 36
1

Borrowing code from your earlier revision I make a simple function that accepts pipeline input. For every object passed we attempt to get a user from Active Directory. I was surprised when setting -ErrorAction on the internal cmdlet didn't work. So the simple solution was to set a try/catch block with not catch action.

Function Get-AdUsers()
{
    [CmdletBinding()]
    Param(
        [Parameter(ValueFromPipeline=$true)]
        $value
    )
    Process {
        try{Get-ADUser $value}catch{}
    }

}

$values = 'gooduser1','badname','gooduser2'
$values | Get-AdUsers

So the above returns 2 user objects. If I remove the try block I still get the same two objects but I also get an error. Using try suppressed it.


In your update code your are getting a terminating error because that is how you defined it. Try just leaving the catch block empty an see if that works.

Matt
  • 45,022
  • 8
  • 78
  • 119
0

You need to write a filtering function, rather than a regular function. To do this put the body of the function in a process block:

function Get-StringLength()
{
    [CmdletBinding()]
    Param
    (
        [Parameter(ValueFromPipeline=$true)]
        $value
    )

    process
    {
        Write-Output $value.Length
    }
}

This will cause the filter to be called once for each item in the pipeline.

Sean
  • 60,939
  • 11
  • 97
  • 136
0

Use either -ErrorAction SilentlyContinue or -ErrorAction Continue :

@('user1', 'user2','user3') | Get-DatabaseUserInformation -ErrorAction Continue

Also, don't use ThrowTerminatingError as I think that shuts down the pipeline. Try throwing a regular exception instead.

Sean
  • 60,939
  • 11
  • 97
  • 136
  • This doesn't work. I've attached an image of what happens. The one in yellow is the one that fails. http://imgur.com/c9qp8Nv – Jake Oct 09 '15 at 15:43
  • @Jake - If you don't want to see the error then use ``-ErrorAction SilentlyContinue` – Sean Oct 09 '15 at 15:45
  • It's still not processing the remaining elements. Once it hits the exception, it terminates the entire cmdlet. – Jake Oct 09 '15 at 15:46
  • throwing the exception seems to shutdown the pipeline as well. – Jake Oct 09 '15 at 15:52