Google
Showing posts with label PowerShell. Show all posts
Showing posts with label PowerShell. Show all posts

2015-11-20

PowerShell Function to Fix System Path

Before Windows 7, max length of the PATH environment variable was 1024 chars. Many years ago, we found ourselves troubleshooting a huge problem in our environment that was caused by faulty install scripts that were appending the PATH variable many times taking the path length over the limit and causing all sorts of problems as PATH was cut at 1024.

Windows 7 increased the length but I've seen the same problem in other environments. I have seen a PowerShell question over at StackOverflow today, and apparently the person was running into the problem because the max path length was exceeded. When that happens, Windows just ignores the characters after the max length is reached.

I had written a PowerShell function that would search for and remove the duplicates that would increase the length of PATH unnecessarily. I put it up as a gist at github & sharing it here. Hopefully it helps someone.

2014-12-11

Even False counts in PowerShell

I was testing one of my PowerShell scripts and noticed a problem, a PowerShell gotcha if you will, that's actually quite easy to miss.

I had something like this in the script:

$rebootGroups=@(([adsisearcher]"(&(objectCategory=computer)(cn=$env:COMPUTERNAME))").FindOne().Properties.memberof -replace '^CN=([^,]+).+$','$1' -match 'grp.reboot.')


That is a one liner to check if the current computer where the script is running is member of any AD groups that starts with 'grp.reboot'.

Later in the script, I had an if statement checking if I got any groups back and act on them:

If ($rebootGroups.count -ne 0) {...}


If computer is indeed in any one of these groups, we would get a count greater than zero.
What happens if computer is not member of any such groups? We might expect to get back a count of '0', but in fact we get a count of '1'.

Here is proof:

PS C:\> $a=@($false)

PS C:\> $b=$false

PS C:\> $c=$true

PS C:\> $a.count;$b.count;$c.count

1

1

1



It could be argued that the first one makes some sense. At the end, it is an [array] type with a single element and hence count should not be 0.

The more interesting is the second one '$b', which is a boolean and regardless of its value ($true or $false), the count property of a boolean is 1.

So, in this case, we would fix the logic by checking if $false was returned, as ($rebootGroups.count -ne 0) will never be equal to 0!

2014-10-29

PwdLastSet vs PasswordLastSet Property

One of the interesting things, when you run "GET-ADCOMPUTER" cmdlet to find out the last time computer password was set, is that there are actually two different properties for that value. One of them, pwdLastSet is NOT so friendly...

D:> Get-ADComputer adil-7600-8 -Properties pwdlastset
pwdlastset        : 130575614253222449


The other property, PasswordLastSet is as helpful as it gets:

D:> Get-ADComputer adil-7600-8 -Properties passwordlastset
PasswordLastSet   : 10/12/2014 12:23:45 AM

Why? Well, I am not sure why Microsoft decided to provide two different properties for this value, probably pwdLastSet is easier for calculations. That is probably why the type of pwdLastSet is signed 64 bit integer (int64) vs. DateTime for PasswordLastSet

D:> Get-ADComputer adil-7600-8 -properties pwdlastset |get-member pwdlastset

   TypeName: Microsoft.ActiveDirectory.Management.ADComputer

Name       MemberType Definition
----       ---------- ----------
pwdlastset Property   System.Int64 pwdlastset {get;set;}

D:> Get-ADComputer adil-7600-8 -properties passwordlastset |get-member passwordlastset


   TypeName: Microsoft.ActiveDirectory.Management.ADComputer

Name            MemberType Definition
----            ---------- ----------
PasswordLastSet Property   System.DateTime PasswordLastSet {get;set;}

2014-05-30

About PowerShell's PSBoundParameters

PSBoundParameters are useful, simple and could trip you!
Here is our test script t.ps1

param(
    $Name="foo",
    $LastName="bar"
)

"Name: $Name LastName: $LastName"
"PSBoundParameters : " 
$PSBoundParameters
    
"Removing 'Name'"
$PSBoundParameters.Remove('Name')

"After Removing 'Name'"
"Name: $Name LastName: $LastName"
"PSBoundParameters : " 
$PSBoundParameters

    
"Removing 'LastName'"
$PSBoundParameters.Remove('LastName')
    
"After Removing 'LastName'"
"Name: $Name LastName: $LastName"
"PSBoundParameters : " 
$PSBoundParameters

## Let's start testing
D:\> C:\temp\t.ps1
Name: foo LastName: bar
PSBoundParameters :      ## <--- It's empty

Removing 'Name'
False                    ## There was no 'Name' parameter to remove

After Removing 'Name'
Name: foo LastName: bar
PSBoundParameters :

Removing 'LastName'
False

After Removing 'LastName'
Name: foo LastName: bar
PSBoundParameters :

D:\>

So, what happened? Well, $PSBoundParameters did not have anything, because nothing was passed to the script.

Now let's try passing it parameters

D:\> C:\temp\t.ps1 -name Adil -LastName Hindistan
Name: Adil LastName: Hindistan
PSBoundParameters :       ## This returns 2 key/value pairs      

Key                                Value
---                                -----
Name                               Adil
LastName                           Hindistan

Removing 'Name'           ## <-- returns True, b/c it could remove the binding
True

After Removing 'Name'
Name: Adil LastName: Hindistan
PSBoundParameters :
LastName                           Hindistan  ## <- now, $PSBoundParameters only has one key/value

Removing 'LastName'
True

After Removing 'LastName'
Name: Adil LastName: Hindistan
PSBoundParameters :

Note that $Name and $LastName got their values assigned and are not affected from $PSBoundParameters.Remove() operation at all. All we are doing is removing keys from a dictionary.

We can also add key/value pairs to it using this:
$PSBoundParameters.Add('Key','Value')

E.g. 
$LastName='Doe'
$PSBoundParameters.Add('NewLastName',$LastName)
$PSBoundParameters are quite useful to determine whether a parameter is using its default value or was it passed a value. It is also useful for splatting:
Some-CommandLet @PSBoundParameters
This supplies the key value pairs a CMDLET or function would require

2014-04-19

PowerShell Gotchas: Enabling Safemode for Windows 8.1 with Bcdedit

In the last couple of days, I started to see a couple of weird issues on my my fully patched Windows 8.1 Update 1 PC. I may blog more about that later but for now, I just want to blog about a PowerShell gotcha, that might frustrate those who are switching from good ol' cmd shell to PowerShell console.

To troubleshoot the recent BSODs, I wanted to enable Safe Mode in Windows 8. Steps are well documented here: http://support.microsoft.com/kb/2809468/en-us

To do this, we need to use BCDEdit utility, which is used to manage Windows boot settings in the newer Windows OSes.

Here I am in PowerShell Admin console...

PS C:\WINDOWS\system32> bcdedit

Windows Boot Manager
--------------------
identifier              {bootmgr}
device                  partition=\Device\HarddiskVolume1
description             Windows Boot Manager
locale                  en-US
inherit                 {globalsettings}
integrityservices       Enable
default                 {current}
resumeobject            {4efceffa-37b1-11e3-9418-e54cd3928210}
displayorder            {current}
toolsdisplayorder       {memdiag}
timeout                 30


Windows Boot Loader
-------------------
identifier              {current}
device                  partition=C:
path                    \WINDOWS\system32\winload.exe
description             Windows 8.1
locale                  en-US
inherit                 {bootloadersettings}
recoverysequence        {8bcbbebd-37b2-11e3-9418-e54cd3928210}
integrityservices       Enable
recoveryenabled         Yes
allowedinmemorysettings 0x15000075
osdevice                partition=C:
systemroot              \WINDOWS
resumeobject            {4efceffa-37b1-11e3-9418-e54cd3928210}
nx                      OptIn
bootmenupolicy          Standard

Let's tell Windows 8.1 to display safe mode options
PS C:\WINDOWS\system32> bcdedit /set {bootmgr} displaybootmenu yes

The set command specified is not valid.
Run "bcdedit /?" for command line assistance.
The parameter is incorrect.


Ugh, ok. I know the command is correct but PowerShell tells us otherwise. Most of the time, dos (I really mean cmd shell) utilities work just fine in PowerShell but when they do not we can tell PowerShell to hand it over to cmd shell
PS C:\WINDOWS\system32> cmd /c bcdedit /set {bootmgr} displaybootmenu yes
The set command specified is not valid.
Run "bcdedit /?" for command line assistance.
The parameter is incorrect.

Same error. OK, I won't keep doing this. You see the curly brackets over there in the command line surrounding bootmgr, that's our problem. PowerShell is trying to parse the arguments and {} is a scriptblock in PowerShell, so we should tell it not to interpret arguments
PS C:\WINDOWS\system32> bcdedit --% /set {bootmgr} displaybootmenu yes

The operation completed successfully.
There! --% was introduced in PowerShell v3 (I believe) and is very useful when handling quotes and such that have a special meaning in PowerShell, and hence parsing them gets hairy.

Note: There are other cases where you need to tell PowerShell to use cmd shell. For example:
d:\>where ssh
## did not return anything
d:\>cmd /c where ssh
D:\Program Files (x86)\Git\bin\ssh.exe

This works because 'where' is an alias for where-object cmdlet in PowerShell. In cmd shell, it tells you where an executable is located (as would `which` in linux/mac)

2014-04-17

Adding reference to PowerShell dll in Visual Studio

Just another quick tip today. I wanted to create a Runspace to run some PowerShell code from C#. When I launched Visual Studio, and tried referencing PowerShell, which lives in System.Management.Automation, all I got was red wiggly underline. Similarly, the PowerShell object was not recognized.



To teach Visual Studio about the System.Management.Automation namespace, I needed to add the relevant DLL, which in my case (as I have x64 Windows 8.1) was found here: 

"%ProgramFiles(x86)%\Reference Assemblies\Microsoft\WindowsPowerShell\3.0\System.Management.Automation.dll"




Once I add it to the References, wigglies go away and I can now use all the namespace has to offer:



Oh, if you are already in PowerShell console, you can see the current assembly location using this:

D:\> [psobject].assembly |fl
CodeBase               : file:///D:/WINDOWS/Microsoft.Net/assembly/GAC_MSIL/System.Management.Automation/v4.0_3.0.0.0__31bf3856ad364e35/System.Management.Automation.dll
EntryPoint             :
EscapedCodeBase        : file:///D:/WINDOWS/Microsoft.Net/assembly/GAC_MSIL/System.Management.Automation/v4.0_3.0.0.0__31bf3856ad364e35/System.Management.Automation.dll
FullName               : System.Management.Automation, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35
GlobalAssemblyCache    : True
HostContext            : 0
ImageFileMachine       :
ImageRuntimeVersion    : v4.0.30319
Location               : D:\WINDOWS\Microsoft.Net\assembly\GAC_MSIL\System.Management.Automation\v4.0_3.0.0.0__31bf3856ad364e35\System.Management.Automation.dll
ManifestModule         : System.Management.Automation.dll
MetadataToken          :
PortableExecutableKind :
ReflectionOnly         : False

2014-02-10

PowerShell WhatIf

There are tons of resources on the web on how to use -whatif parameter in Powershell functions. I won't go over them but here is a mistake I noticed people sometimes do: They forget to add SupportsShouldProcess to the main script.




Couple of things to note here:

* When you pass -whatif parameter to the main script, it gets passed down to any 'advanced' function that is also supporting whatif.

In out example, Test-WhatifTraversing function support -whatif, because it too has the following line

[CMDLETBINDING(SupportsShouldProcess)]

* Once you use -whatif to call your script, it will not only pass it to Advanced Functions, but it will automatically execute on any other native PowerShell cmdlets that supports -whatif

* You want to wrap the 'dangerous code' you have in that 'If' block so that when you pass -whatif it does NOT execute, but even if you don't have any such code, it is still beneficial to add 'SupportsShouldProcess' to all your functions and scripts because of what I mentioned above.

2014-01-21

dsget parsing in PowerShell vs cmd

Recently, we had a discussion in a Microsoft Certified Professionals group about how to detect who joined a computer to domain. One of the folks suggested using 'qused' option of 'dsget'. It justs returns a number, so you really need to get the count for all users and then see whose number got higher etc., but that's not the reason I am jotting this down.

It's about an unhelpful message from dsquery and how PowerShell parsing of commands from cmd.exe could be problematic.

So the command we are interested in is:
dsget user <DN> -part <AD Partition> -qused -display

E.g.
dsget user "CN=adil,OU=xyz Users,DC=xyz,DC=org" -part dc=xyz,dc=org -qused -display


Instead of typing  DistinguishedName of the user, it is also possible to get that from dsquery and then pipe it to dsget:

dsquery user -samid <KerberosID> | dsget user -part <AD Partition> -qused -display


E.g.:
dsquery user -samid adil | dsget user -part dc=xyz,dc=org -qused -display
 display         qused
 Hindistan, Adil 700

dsget succeeded


Note that there is a space between 'xyz users', so we enclosed that part in quotes for cmd to parse it correctly. Otherwise you would get an error:

dsget failed:Value for 'Target object for this command' has incorrect format.


If you however run the last command in PowerShell, it will not work:

PS H:\> dsquery user -samid adil | dsget user -part dc=xyz,dc=org -qused -display
  dsget failed:A referral was returned from the server.
  type dsget /? for help.


To error becomes more meaningful if you omit the dsquery part and just run dsget:

PS H:\> dsget user "CN=adil,OU=xyz Users,DC=xyz,DC=org" -part dc=nyumc,dc=org -qused -display

 dsget failed:'dc=org' is an unknown parameter.

 type dsget /? for help.


So, PowerShell is looking at the part after -part and does not interpret it as cmd.exe would. There are several ways to fix this:

1) Simply use quotes or single quotes around the part after -part
PS H:\> dsget user "CN=adil,OU=xyz Users,DC=xyz,DC=org" -part "dc=nyumc,dc=org" -qused -display


The other option is to tell PowerShell to run cmd.exe and let it parse the rest using call operator:

PS H:\> &cmd.exe /c 'dsget user "CN=adil,OU=xyz Users,DC=xyz,DC=org" -part dc=nyumc,dc=org -qused -display'
Display         qused
Hindistan, Adil 700
dsget succeeded

And here is a little function I wrote to beautify it (yep, I know, beauty is in the eye of the beholder)

function Get-DomainJoinCount {
 param (
          [Parameter(ValueFromPipeline)]
          [Alias('ID','SamID')][string]$KerberosId='adil'
       )
 dsquery user -samid $KerberosID |
 dsget user -part "dc=xyz,dc=org" -qused -display).trim() -replace '\s{2,}',';' | select -skip 1 -First 1 |
 ConvertFrom-Csv -Delimiter ';' -Header "User Name","Count"
}

PS H:\> Get-DomainJoinCount adil |ft -AutoSize

User Name        Count
---------        -----
Hindistan, Adil 700 

2014-01-19

Updating Environment Variables

I installed Python 2.7 on my computer, but install path was not added to environment variables. A couple of quick notes on doing this with PowerShell:

  • PowerShell does not offer a direct cmdlet to manipulate $env:path that would stick after the shell is closed.
  • [System.Environment]::GetEnvironmentVariable() and [System.Environment]::SetEnvironmentVariable() .NET methods are the way to go about this. 
  • Standard PowerShell window would not work in this case, and you need to be on an elevated PowerShell window.
  • You can also directly manipulate the registry key which holds system-wide environments variables [HKLM\System\CurrentControlSet\Control\Session Manager\Environment\Path]

To get the current path, you can either use .NET method or built in $ENV variable:

PS C:\> [System.Environment]::GetEnvironmentVariable('PATH')
C:\Program Files (x86)\AMD APP\bin\x86_64;C:\Program Files (x86)\AMD APP\bin\x86;...

PS C:\> $env:Path
C:\Program Files (x86)\AMD APP\bin\x86_64;C:\Program Files (x86)\AMD APP\bin\x86;...

Note that, with the .NET [System.Environment], you have also the option of getting USER variable only:

PS C:\[System.Environment]::GetEnvironmentVariable('PATH','USER')
C:\Program Files (x86)\Google\google_appengine\

To add my Python Path (c:\Python\27):

PS C:\> [System.Environment]::SetEnvironmentVariable('PATH',$env:Path + ';C:\Python\27', 'MACHINE')


It's important to remember that when you are trying to make system-wide changes, or sometimes even to access system data, you need to be on an elevated shell, which you can get by right clicking PowerShell launcher and choosing 'Run As Administrator'

Most of the time, PowerShell will give you hints that you need to be on an elevated shell but not always. For example, if you run the command above in a regular shell, you do not get an error. In fact, the change will seem to go through, except it won't and when you launch a new shell, you will notice that path you added is not there.

Below is an example where you do get an error if you attempt to run it in a non-elevated shell:

function get-smart {
gwmi -namespace root\wmi -class MSStorageDriver_FailurePredictStatus |
        select InstanceName,PredictFailure,Reason |ft -a
}

Regular shell will 'yell' at you in red:

PS C:> get-smart
gwmi : Access denied 
At line:2 char:2
+     gwmi -namespace root\wmi -class MSStorageDriver_FailurePredictStatus |select In ...
Run the same function in an elevated window:

PS C:\> get-smart

InstanceName                                                              PredictFailure Reason
------------                                                              -------------- ------
SCSI\Disk&Ven_Hitachi&Prod_HDT721010SLA360&Rev_ST6O\5&24ba3dff&0&000000_0          False      0
SCSI\Disk&Ven_&Prod_ST31500341AS\4&5ecf4f&0&020000_0                               False      0

By the way, that function will check the SMART status of your hard drives and tell you whether SMART expects any failure

2014-01-15

Fixing quser access is denied error

Daily PowerShell tips from PowerShell.com are a great way to learn PowerShell tips and tricks. They sent a tip yesterday about 'quser'. If you have been around for a while, you may remember that command. A long long time ago, Citrix helped Microsoft to create 'Terminal Server'. They also created some 'q****' commands.

If you go into your system32 directory and list executables that start with q, you will some others, like qwinsta.exe and qprocess.exe, as well as quser.exe Microsoft kept these executables ever since.

I am familiar with them, because many years ago I had written a perl application to monitor Citrix Servers and these commands came handy at the time.

Anyway, PowerShell tip yesterday was about finding out logged on user:

PS H:\> quser  
USERNAME              SESSIONNAME        ID  STATE   IDLE TIME  LOGON TIME
  >adil              console             1  Active      none   1/15/2014 11:35 AM PS 

H:\> (quser) -replace '\s{2,}',','|ConvertFrom-Csv
 SERNAME    : >adil
SESSIONNAME : console
ID          : 1
STATE       : Active
IDLE TIME   : none
LOGON TIME  : 1/15/2014 11:35 AM


The first command shows what you would have seen by running the command on the current machine, and Tobias Weltner's tip shows us how to first replace 2 or more spaces with ',' and then, use ConvertFrom-CSV cmdlet to convert string into a reusable PowerShell Object (PSObject).

Today, there was a follow up on the tip to find out who logged on on a remote computer, using /server parameter. One of my colleagues tried it and and reported that it was not working for her.





Error 0x000006BA enumerating sessionnames
Error [1722]: The RPC server is unavailable.


The first line of error suggested that it was a permissions issue, but she was an admin on the remoe windows 7 box.
The second line was telling us that the remote machine was not responding to Remote Procedure Call (RPC).

So, to fix this, we needed to enable RemoteRPC calls in the registry of the remote machine.
PS H:\> invoke-command -computername adil-w7x32vm2 -Command { set-itemproperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server' -Name AllowRemoteRPC  -Value 0x1 -Force }

 And once the RemoteRPC is allowed, query goes through without any errors.

PS H:\> (quser /server:adil-w7x32vm2) -replace '\s{2,}',','|ConvertFrom-Csv

  USERNAME    : testuser
  SESSIONNAME : console
  ID          : 3
  STATE       : Active
  IDLE TIME   : none
  LOGON TIME  : 1/15/2014 10:29 AM
 Note that, you migh also use the following command to connect to remote registry and change the setting, in case you cannot use invoke-command:

reg add "\\ComputerName\HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v AllowRemoteRPC /t Reg_Dword /d 0x1 /f

And query to make sure the change took place:

PS H:\> reg query "\\adil-w7x32vm2\HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server" /v AllowRemoteRPC

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server

    AllowRemoteRPC    REG_DWORD    0x1

2014-01-09

Finding and dealing with a special char in PowerShell

Recently I ran into an issue while doing regex on Bios Serial Numbers. A couple of machines returned a special character as their serial number when I ran the following command

(Get-WmiObject Win32_Bios) .SerialNumber

�����

Not sure if it will show up fine on your computer but it looks like a black diamond shape with question mark in it.

For my purpose, I needed to eliminate such computers from my results, so naturally I wanted to be able to match them in my regular expression.

The problem was that I had no idea how to produce it in my regex.
I knew I could have produced any character using this

[char]'CharCodeGoesHere'

But  how do I find the char code? Here is how:

I copied the first '?' character and then pasted it between the single quotes (could have been double quotes but in general if you do not want something to be evaluated, avoid double quotes) below in PowerShell

PS D:\> [int][char]'�'
65533

So I could reproduce the weird character by dropping [int]

PS D:\> [char]65533


This meant that I could now use it in regular expression like this:

PS D:\>(Get-WmiObject Win32_Bios) .SerialNumber -match "$([char]65533)"
True

While we are at it, here are a couple of valid and invalid ways to do this comparison

PS D:\> '�' -match "$([char]65533)"
True

PS D:\> "�" -match [char]65533
True

PS D:\> '�' -match [char]65533
True

PS D:\> '�' -match '[char]65533'  (Included this because single quote some times trip people. PowerShell does not evaluate the stuff inside single quotes.

PS D:\> � -match "$([char]65533)"
� : The term '�' 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.
At line:1 char:1
+ � -match "$([char]65533)"
+ ~
    + CategoryInfo          : ObjectNotFound: (�:String) [], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

In the last example above, Left side of -match operator is not enclosed within any quotes and therefore an error is generated. Error is quite self explanatory.

2013-10-16

Permissions to access WMI remotely

Accessing WMI on a remote machine is pretty mundane for System Admins. They do not even think about it, they have Administrator rights, and it works (for the most part).

In my case, I needed to run a script with a service account and connect from one Windows 2008 R2 Server to another. I needed to figure out what permissions that account should have on the server, short of making the service account an admin. How difficult this can be? Well, it proved to be more difficult to find information on this than I thought it would be.

Here is what happens if the service (domain) account I use tries to access remote server, where it does not have any permissions:

PS> gwmi win32_ComputerSystem -ComputerName test.adilhindistan.com
gwmi : Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
At line:1 char:1
+ gwmi win32_ComputerSystem -ComputerName test.adilhindistan.com
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-WmiObject], UnauthorizedAccessException
    + FullyQualifiedErrorId : System.UnauthorizedAccessException,Microsoft.PowerShell.Commands.GetWmiObjectCommand
I looked at several resources including venerable StackOverflow, where I have found several responses to this type of questions but none worked for my case.

There are many KB articles on the subject. This dated MSDN article claimed that you need to be admin on the target box. WMI Troubleshooting, Connecting to WMI on a Remote Computer by Using Windows PowerShell,

I asked in PowerShell Community Group chat, where Joel "Jaykul" Bennett pointed me to this article.

All of them helped, but in the end, I had to find my own solution to it:

1) Add the user to 'Distributed DCOM Users' group on the target server
2) Grant WMI permissions to user by following the steps below:

  • Launch compmgmt.msc and connect to target server
  • Right click on Services and Applications > WMI Control and select "Properties"
  • Click Root (CIMV2 did not seem to work but see update below) and then "Security"
  • Add the domain user and click on "Advanced"
  • Double click on user name
  • Change Applies to to "This namespace and subnamespaces"
  • Click on "Remote Enable" checkbox and hit "OK"
See screenshots below.



Note that I only needed read access. If you need to write or execute WMI methods, then additional checkboxes (Execute Methods, * write) will need to be added.

Update 2013-10-17:
I got an e-mail from Colyn, who had helped answering the question on StackOverflow thread asking me if  adding user to "WinRMRemoteWMIUsers__" group would work for me.

I did not have that option because that group did not exist on the server, which had PowerShell version 2. CIM cmdlets became available with PowerShell v3, so I guess that explained why.

However, I had access to another Windows 2008 R2 server with PowerShell3. Sure enough, that server had the group. the group definition says:
"Members of this group can access WMI resources over management protocols (such as WS-Management via the Windows Remote Management service). This applies only to WMI namespaces that grant access to the user."

On target server:
1) I added domain test user to  "WinRMRemoteWMIUsers__".
2) I granted both "Enable Account" & "Remote Enable" access to  Root/CIMV2 namespace

FAILURE:
Get-WMIObject call failed with the same access denied error:

PS D:\> get-wmiobject win32_ComputerSystem -Computer TEST01
gwmi : Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
At line:1 char:1
+ gwmi win32_ComputerSystem -Computer TEST01
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [Get-WmiObject], UnauthorizedAccessException
    + FullyQualifiedErrorId : System.UnauthorizedAccessException,Microsoft.PowerShell.Commands.GetWmiObjectCommand

Similarly, Get-CimInstance with DCOM protocol fails as well

PS D:\> $cimoption = New-CimSessionOption -Protocol Dcom
PS D:\> new-cimsession -computername wstwsdcpvm001.nyumc.org -SessionOption $cimoption
new-cimsession : Access is denied.
   + FullyQualifiedErrorId : HRESULT 0x80070005,Microsoft.Management.Infrastructure .CimCmdlets.NewCimSessionCommand


SUCCESS:
But Get-CimInstance cmdlet which uses WS-MAN protocol succeeded:

PS D:\> get-ciminstance -classname win32_ComputerSystem -ComputerName TEST01

Name             PrimaryOwn Domain     TotalPhysi Model     Manufactu PSCompute
                 erName                calMemory            rer       rName
----             ---------- ------     ---------- -----     --------- ---------
TEST01    Windows...   4026064896 VMware... VMware... TEST01...

NO ALIAS PLEASE!
Here is another twist to this story. If I use the dns alias instead of actual hostname, Get-CimInstance too returned an error, but return code is different (0x80070035).

WMI SERVICE GOTCHA
Another note: if you will be playing with the WMI permissions, you may sometimes see unexpected results if you do not restart WMI service (get-service winmgmt | restart-service -force)  after making a change. For example: I wanted to test removing "Remote Enable" permission and see if I would still be able to use Get-CimInstance. To my surprise it worked for a while...until I restarted the service. Then I started to get errors (HRESULT 0x80041004). I had to add back the "Remote Enable" and restart WMI service to get it back up.

Finally, here are a couple more useful links:

2013-10-08

Detecting and Fixing Duplicate SPN

If you search internet, you will find tons of people trying to find an answer why and how a computer trust relationship is broken and as a result Windows rudely refuses to log them in.

The error message: "The security database on the server does not have a computer account for this workstation trust relationship"


Seeing the message, the first suspicion is that something is wrong with the secure channel that the computer uses to communicate with Active Directory.

I logged into the computer using a local account, launched PowerShell to check the status of Secure Channel:

PS> Test-ComputerSecureChannel
True

OK. That is not the issue then! If it had returned "False", I could have used the -repairChannel parameter to fix it (need to run that in PowerShell Admin console).

PS> Test-ComputerSecureChannel -Repair

BTW, good old 'nltest' can be used to test and reset secure channel too, as shown below, but no need to bother when you have PowerShell?

PS H:\> nltest /sc_query:adilhindistan.com
Flags: 30 HAS_IP  HAS_TIMESERV
Trusted DC Name \\MyDCName.adilhindistan.com
Trusted DC Connection Status Status = 0 0x0 NERR_Success
The command completed successfully

Similarly, the following would reset it.

nltest /sc_reset:adilhindistan.com


Looking at Event logs revealed that this was related to an Service Principal Name issue:

WORKSTATION:
Log Name:      System
Source:        Microsoft-Windows-Security-Kerberos
Date:          10/7/2013 3:59:14 PM
Event ID:      3
Task Category: None
Level:         Error
Keywords:      Classic
User:          N/A
Computer:      testcomputer.adilhindistan.com
Description:
A Kerberos Error Message was received:
on logon session 
 Client Time: 
 Server Time: 20:59:14.0000 10/7/2013 Z
Error Code: 0x7  KDC_ERR_S_PRINCIPAL_UNKNOWN
Extended Error: 0xc0000035 KLIN(0) <===== STATUS_OBJECT_NAME_COLLISION (Object Name Already Exists)
Client Realm: 
 Client Name: 
 Server Realm: adilhindistan.com
Server Name: host/testcomputer.adilhindistan.com
Target Name: host/testcomputer.adilhindistan.com@adilhindistan.com
Error Text: 
 File: 9
Line: f09
Error Data is in record data.


DOMAIN CONTROLLER:
Log Name:      System
Source:        Microsoft-Windows-Kerberos-Key-Distribution-Center
Date:          10/7/2013 3:59:14 PM
Event ID:      11
Task Category: None
Level:         Error
Keywords:      Classic
User:          N/A
Computer:      mydc.adilhindistan.com
Description:
The KDC encountered duplicate names while processing a Kerberos authentication request. The duplicate name is host/testcomputer.adilhindistan.com (of type DS_SERVICE_PRINCIPAL_NAME). This may result in authentication failures or downgrades to NTLM. In order to prevent this from occuring remove the duplicate entries for host/testcomputer.adilhindistan.com in Active Directory.

I ran setspn command to show me the duplicate SPNs:

setspn.exe -X -P

Looked at results, yet the computername I was concerned was not listed. By the way, there is a detailed Microsoft article on SPN and setspn.exe usage here.

setspn.exe -Q HOST/testcomputer

Checking domain DC=adilhindistan,DC=com
CN=testcomputer,OU=Workstations,DC=adilhindistan,DC=com
        TERMSRV/testcomputer.adilhindistan.com
        WSMAN/testcomputer.adilhindistan.com
        RestrictedKrbHost/testcomputer.adilhindistan.com
        HOST/testcomputer.adilhindistan.com
        TERMSRV/testcomputer
        WSMAN/testcomputer
        RestrictedKrbHost/testcomputer
        HOST/testcomputer

Existing SPN found!

That showed me the current SPN and it looked right but did not help with detecting the computer that's causing the conflict. This did the trick:

setspn.exe -Q HOST/testcomputer.adilhindistan.com

Checking domain DC=adilhindistan,DC=com
CN=testcomputer1,OU=Workstations,DC=adilhindistan,DC=com
        HOST/testcomputer.adilhindistan.com
        HOST/testcomputer1

Checking domain DC=adilhindistan,DC=com
CN=testcomputer,OU=Workstations,DC=adilhindistan,DC=com
        TERMSRV/testcomputer.adilhindistan.com
        WSMAN/testcomputer.adilhindistan.com
        RestrictedKrbHost/testcomputer.adilhindistan.com
        HOST/testcomputer.adilhindistan.com
        TERMSRV/testcomputer
        WSMAN/testcomputer
        RestrictedKrbHost/testcomputer
        HOST/testcomputer

Existing SPN found!

So, 'testcomputer1' was incorrectly claiming the SPN HOST/testcomputer.adilhindistan.com and causing the issue.

While on the subject here is another link that might help in other relevant cases:
Kerberos Error Code: 0x7 KDC_ERR_S_PRINCIPAL_UNKNOWN

2013-10-05

The server is unwilling to process the request

I was working on a script that reads the information about printers on a print server and creates AD groups for each share name if it does not exist. Script is part of a process to manage shared printers for users.

So, the part of code goes like this:

try {
    New-AdGroup -Path "OU=Printers,OU=Groups,DC=XYZ,dc=org" -Name $PrinterGroupName -GroupScope DomainLocal -GroupCategory Security -Description $Description -OtherAttributes @{Info=$Comment}
    }
catch { $_ }

There is a line above that code to get all printers on the Print Server:
 Get-Printer -ComputerName $PrintServer   ## Available on Win8+
 

Then this code retrieves Printer Group Name, Printer Location as Description 'AND' a comment field out of 'Get-Printer' cmdlet results and populates relevant fields in the new group.

This code was failing to create some groups and the error message was quite unhelpful: "The server is unwilling to process the request"

I looked at the groups and realized that New-ADGroup was not happy when $comment did not include anything.

So, I added a couple of lines before try/catch block to make sure I would have something for both Active Directory Group's Description and  Notes fields to fix the issue:

$Description = $Comment = $printer.name

if ($printer.location) {

   $Description = $printer.location

}

If ($Printer.Comment) {

  $Comment = $Printer.Comment

}

2013-09-27

Issue with PowerShell 4 AD Module on Windows 8.1

Just a quick note that I am seeing some issue with the new PowerShell 4.0 ActiveDirectory Module on Windows 8.1 when retrieving user properties.

PS> get-aduser adil -properties *

get-aduser : One or more properties are invalid.

Parameter name: msDS-AssignedAuthNPolicy

At line:1 char:1

+ get-aduser adil -properties *

+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

    + CategoryInfo          : InvalidArgument: (hindia01:ADUser) [Get-ADUser], ArgumentException

    + FullyQualifiedErrorId : ActiveDirectoryCmdlet:System.ArgumentException,Microsoft.ActiveDirectory.Management.Commands.GetADUser

The property 'msDS-AssignedAuthNPolicy' it is complaining about seems to be a new one implemented on Windows Server 2012 R2, according to MSDN: http://msdn.microsoft.com/en-us/library/dn366549.aspx
You can specify the properties you want manually, except that one of course. Hopefully, Microsoft will fix it by the time Windows 8.1 GA on October 18, 2013.

Update 2014-01-09:
I kinda forgot about this issue but it's still not fixed. You can workaround this issue by piping the user object to Get-ADObject cmdlet:

Get-ADUser Adil | Get-ADObject -properties *



By the way, Get-ADComputer cmdlet has the same issue and same workaround can be applied to it.
There is also a bug filed for it here: https://connect.microsoft.com/PowerShell/feedback/details/806452/windows-8-1-powershell-4-0-get-adcomputer-properties-bug#

Update 2014-03-15
Fixed: http://support.microsoft.com/kb/2923122

Update 2014-03-17
Unfortunately the fix does not seem to work for me. I installed the update rollup March 2014 via WSUS so I already had i for a few days and file information is listed here: http://support.microsoft.com/kb/2928680

I have these files (see the screenshot below) yet I still get the same error as before.


2013-09-02

PowerShell try catch gotcha

I check out StackOverflow's PowerShell section time to time, and one issue seems to trip people a lot: Try/Catch block.

Here is how Try/Catch is block is constructed:


Try {
## Some code here which is likely to fail
}
Catch {
## What to do if try block has a "terminating" error
}

Here is an example:


try { get-content c:\a.txt;"file exists" }catch{"that file does not exist"}

If you run the code above; it will throw an error on the Get-Content if 'c:\a.txt' file does not exist. And in theory we should be able to catch that but that is not exactly what happens:


PS Z:\> try { get-content c:\a.txt;"file exists" }catch{"that file does not exist"}
get-content : Cannot find path 'C:\a.txt' because it does not exist.
At line:1 char:7
+ try { get-content c:\a.txt;"file exists" }catch{"that file does not exist"}
+       ~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (C:\a.txt:String) [Get-Content], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.GetContentCommand

file exist
PS Z:\>

Did you notice that "file exist" line being printed? Umm, what happened?

Well, remember I said "terminating" errors are caught. the first line is an error but not a terminating error. And therefore, the next line is executed.

Anything we can do to stop this? Yes, use -ErrorAction after the command or $ErrorActionPreference.


PS Z:\> try { get-content c:\a.txt -ErrorAction stop; "file exists" }catch{"that file does not exist"}
that file does not exist
PS Z:\>

With ErrorAction set to STOP, the error on the first line becomes a terminating error and hence get caught in the CATCH {} block.

Similarly, we can set the $ErrorActionPreference variable before the line top "STOP"; and tell our code to treat any error as terminating one.


PS Z:\> $ErrorActionPreference="STOP"; try { get-content c:\a.txt; "file exists" }catch{"that file does not exist"}
that file does not exist
PS Z:\>

If not specified, $ErrorActionPreference is set to "Continue".


PS Z:\> $ErrorActionPreference
Continue
PS Z:\>

Note that it is not "SilentlyContinue", which would execute the next statement without complaining about the error.


PS Z:\> try { get-content c:\a.txt -ErrorAction SilentlyContinue; "file exists" }catch{"that file does not exist"}
file exist

This is equivalent to "On Error Resume Next" in vbScript.

2013-08-10

Printer Connection Issues on XP

I've been hitting my head against the wall with the inconsistencies I see when trying to use Microsoft printui.dll to connect network printers on XP machines. Here goes the story...

Problem:

There are a bunch of "floor" printers that are published in a Windows 2008 R2 print Queue. We want every user who logs on to the computers on that floor to automatically get these printers.

Solution:

There are several well known ways, I won't go into details. Here is a nice blog that goes over them: http://blog.powershell.no/category/print-management/

In my case, I came up with a naming schema for printer groups

  • grp.prn.PrintServerName.PrinterShareName 

ComputerNames are added into these groups, which I then parse through a startup script and use well-known printui.dll to connect the machine to these printers. There were several technical but of course also some political reasons to do it this way. Your mileage may vary.

Windows 7 & PowerShell

Anyway, I wrote a Computer Startup PowerShell script that is deployed via Group Policy, put the logic in to detect which printers the computer should be connecting to, and wrapped it around the command below:

  • printui.exe /ga /n\\PrintServer\Printer

and all went well.

The switch (ga) is causes "per-machine" connection. Underneath, it caused creation of a registry key entries under HKLM and once users are logged in it "pushes" these connections to user, so that user is able to see and print to those printers without requiring them do anything.

Windows XP, still around...

XP is another story. Well, for one, xp does not have printui.exe but its older cousin rundll32 printui.dll,PrintUIEntry, which is well documented on technet, is still around and unfortunately, there are several XP machines without PowerShell in the environment, so I had to go back to vbscript.

It's a similar story, I back-ported the logic and script to vbscript:

  • rundll32 printui.dll,PrintUIEntry /ga /n\\PrintServer\PrinterName 

Note that, technically, it's not the printer name we use but the Printer Share Name, which may be different than the printer name). Tested it, yep works.

A couple of days later, I hear from one of the techs that printers are not getting mapped for his computers. Script logging clearly showed that it ran the command. Problem is, command does not have a return code to tell us whether command was successful or not. I could not find any documentation that explains the details of what goes on at that point.

Perhaps a debug flag somewhere would be useful but spooler is a very magical and at the same time finicky service especially on Windows XP. One thing is certain, it is inconsistent and there is no way to know why (yet)...

I hit other weird issues during troubleshooting. Below is one of them and the workaround.

Issue:

I wanted to remove some of the printers I connected to. Because these printers are connected per-machine using /ga switch, they need to be removed using /gd switch.

So, I run the appropriate command

  • Locally: rundll32 printui.dll,PrintUIEntry /gd /n\\PrintServer\PrinterName
  • Remotely: rundll32 printui.dll,PrintUIEntry /gd /c\\TargetComputer /n\\PrintServer\PrinterName


Printer connection cannot be removed. Operation could not be completed.

Workaround:

The fastest workaround I found for this issue is delete the relevant registry keys that are under:

  • HKLM\System\CurrentControlSet\Control\Printer\Connections\


Below are some of the other useful resources on Printing


Sample VBScript:

On error resume next
ScriptVersion="2013-08-09-02"
Set oShell = CreateObject("WScript.Shell")
oShell.LogEvent 0, "WinXP_WSStartup.vbs: Script Version: " & ScriptVersion

Set objSysInfo = CreateObject("ADSystemInfo")
Set objComputer = GetObject("LDAP://" & objSysInfo.ComputerName)

Groups = objComputer.GetEx("MemberOf")
' If Above Command does not find any groups for computer, it will error
if Err.Number <> 0 Then
oShell.LogEvent 0, "WinXP_WSStartup.vbs: Computer is not member of any AD groups. Exiting."
Wscript.Quit
end if

isFirst=vbTrue
Count=0
For Each group In Groups
group=Replace(group,"/","\/") ' escape groups with a slash in the object name
Set objGroup = GetObject("LDAP://" & group)
If ( Instr(LCase(objGroup.CN),"grp.prn." ) > 0 ) then
If isFirst Then
PrinterGroups = objGroup.CN
isFirst=vbFalse
Count=1
Else
PrinterGroups = PrinterGroups & ";" & objGroup.CN
Count=Count+1
End If
End if
Next

if (Count > 0) Then
arrPrinterGroups=Split(PrinterGroups,";")
for each PrinterGroup in arrPrinterGroups
arrGroup = Split(PrinterGroup,".")
' skipping first 2 grp and prn as groups are named grp.prn.ServerName.PrinterName
Server=arrGroup(2)
Printer=arrGroup(3)

cmdConnectPrinter="cmd /c rundll32 printui.dll,PrintUIEntry /ga /n\\" & Server & "\" & Printer
oShell.LogEvent 0, "WinXP_WSStartup.vbs: Connecting to Printer: " & Printer & " on Server: " & Server
oShell.LogEvent 0, "WinXP_WSStartup.vbs: Running " & cmdConnectPrinter

' Hide Window and return immediately without waiting
oShell.Run cmdConnectPrinter,0,false

Next
End If

oShell.LogEvent 0, "WinXP_WSStartup.vbs: Nothing else to do. Exiting"
Set oShell=Nothing

*******
In PowerShell, I wrote a couple of functions, thinking I might use the same logic to add user-side printer connections, hence the 'user' & 'computer' objectCategories below.

A single line calls the functions:


Connect-NetworkPrinter -objectCategory computer


Here are the functions:


Function Connect-NetworkPrinter {
[CMDLETBINDING()]
  Param(    
    [ValidateSet('user','computer')]
    [String]$objectCategory='computer',

[string]$PrinterGroupIdentifier='^cn=grp.prn.',
[string]$name )
if (!$name) { if ($objectCategory -eq 'computer') { $name=$($env:COMPUTERNAME) } if ($objectCategory -eq 'user') { $name=$($env:USERNAME) } } Write-Verbose "Connect-NetworkPrinter: PrinterGroupIdentifier set to $PrinterGroupIdentifier" Write-Verbose 'Connect-NetworkPrinter: Calling Function to Get Group Membership'
$groups = Get-ADGroupMembership -objectCategory $objectCategory -name $name
if (!$groups) { Write-Verbose "Connect-NetworkPrinter: No groups returned for current computer" logMsg "Connect-NetworkPrinter: No groups returned for current computer" return $false }
$printergroups=@() foreach ($group in $groups) {
Write-Verbose "Connect-NetworkPrinter: Checking group $group" if ($group -match $PrinterGroupIdentifier) { Write-Verbose "Connect-NetworkPrinter: Matches criteria $group"
$Server,$Printer,$null=($group -replace $PrinterGroupIdentifier -replace ',ou=.*$').split('.')
Write-Verbose "Connect-NetworkPrinter: Connecting to Printer: $Printer on Server: $Server" try { Invoke-Expression 'rundll32 printui.dll,PrintUIEntry /ga /n"\\${Server}\${Printer}"' ## rundll32 does not return anything so cannot check on error logMsg "Connecting to Printer: $Printer on Server: $Server" } catch { Write-Error "An error occured $_" logMsg "Error occured connecting to Printer: $Printer on Server: $Server" -entrytype 'Error' }
# [PSCustomObject]@{Server=$server;printer=$printer} ## return PSCustomObject } } Write-Verbose 'Connect-NYUPrinter: Exiting' logMsg "Printer connections complete"
}
function Get-ADGroupMembership { <# .SYNOPSIS Gets Active Directory Group membership for users or computers
.EXAMPLE Get-ADGroupMembership -name 'user1','user2' -Verbose Displays group membership for users user1 and user2
.Parameter name Name of the computers or users
.Parameter ObjectCategory Can be user or computer. By default it is 'computer'
.NOTES Output is string array of Distinguished Names. If nothing is found, it returns $false #>
[cmdletbinding()] param([Parameter(Mandatory=$true)] [string[]]$name, [ValidateSet('user','computer')] [String]$objectCategory='computer' ) Begin { Write-Verbose 'Get-ADGroupMembership: Creating Directory Search Object' $searcher=New-Object DirectoryServices.DirectorySearcher }
Process { $name | % { Write-Verbose "Get-NYUGroupMembership: Processing $_" $filter="(&(objectCategory=$objectCategory)(cn=$_))" ## this will only find user object $searcher.PageSize=1000 $searcher.Filter=$filter $result=$searcher.FindOne()
if ($result) { $result.Properties.Item("memberOf") } else { Write-Verbose '...is not member of any groups' } } }
END { Write-Verbose 'Get-ADGroupMembership: Disposing Directory Search Object' $searcher.Dispose() } }
I am not posting here the 'logmsg' function that the PowerShell functions are referring to, but it is basically just logging events into a Special Log I set up for all my scripts. It's so old, I did not even bother to name it PowerShell-way.

Enjoy!

2013-04-01

Change Location Settings via PowerShell

Today, I have seen a question on StackOverflow.com about how to turn on/off Location Settings in Windows 8 via PowerShell.

This setting is available in the Control Panel (Win + I > Control Panel), under "Location Settings".


To be able to locate the relevant registry key, I launched Procmon (aka. Process Monitor) from SysInternals. Anyone who used the application knows how overwhelming the data will be in just a few seconds of capture. So I had to be quick:

  • I started the capture, 
  • immediately cleared the checkbox next to "Turn on the Windows Location Platform", 
  • clicked Apply
  • And stopped Capture

Even though all that took a few secs, capture generated tens of thousands of events. I started from bottom, excluding all the irrelevant Operations, until I reached the one I am looking for "RegSetValue"





The path it referred to sounded promising:
HKLM\Software\Microsoft\Windows NT\CurrentVersion\Sensor\Overrides\{GUID}\SensorPermissionState

I checked another Windows 8 machine and confirmed that the GUID was the same:
Then, manually changed registry key from 0 to 1 and refresh Location Setting to confirm checkbox was back on.


Once that was done, doing it in PowerShell was quite trivial:

To turn on the Windows Location Platform set value to 1:


set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Sensor\Overrides\{BFA794E4-F964-4FDB-90F6-51056BFE4B44}' -Name SensorPermissionState -Value 0x1

To turn off the Windows Location Platform set value to 0:


set-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Sensor\Overrides\{BFA794E4-F964-4FDB-90F6-51056BFE4B44}' -Name SensorPermissionState -Value 0x0

2013-01-11

Getting Members of Large Groups via PowerShell


PowerShell AD module make checking group membership a trivial task. If I wanted to get the list of members for a group, I can use Get-ADGroupMember cmdlet. Problem with module is that, it's an 'extra' component on top of standard installation and if you will use it, you need to make sure that your script will have access to it. You may be using PowerShell as your logon/start-up script and your machines may not have the AD module. Yes, there are techniques remote-session techniques that can be employed to work around this but most would prefer to enumerate group members some other way.

One common method is using .Net classes, specifically "DirectoryServices.DirectorySearcher". I had a function that was using this class to enumerate group members, something like this:

function Get-GroupMembers {

  param ([string]$group)

  $searcher=new-object directoryservices.directorysearcher   
  $filter="(&(objectClass=group)(cn=${group}))"
  $searcher.PageSize=1000
  $searcher.Filter=$filter
  $result=$searcher.FindOne()
  $members = $result.properties.item("member")
  if ($members) {
    return $members
  }

  return $false

}


I noticed that this stopped working for a specific group, and when I manually looked into the group properties,  I found that "member" property of the group was empty. There was; however, another noticeable property that got populated instead: "member;range=0-1499".

Long story short, when members of a group exceed 1500, AD was populating that property instead of plain 'member' property and I needed to use a different technique called 'ranged retrieval'.

The main change is that we use 'PropertiesToLoad' property to feed $searcher object the range of members we would like to get and loop through them. When we give $searcher object an invalid range, it throws an error at us and we catch that to exit the loop. Here is the modified function.

## Return members of given AD group

function  Get-GroupMembers {

    param ([string]$group)

    if (-not ($group)) { return $false }
  

    $searcher=new-object directoryservices.directorysearcher   
    $filter="(&(objectClass=group)(cn=${group}))"
    $searcher.PageSize=1000
    $searcher.Filter=$filter
    $result=$searcher.FindOne()

    if ($result) {
        $members = $result.properties.item("member")

        ## Either group is empty or has 1500+ members
        if($members.count -eq 0) {                       

            $retrievedAllMembers=$false           
            $rangeBottom =0
            $rangeTop= 0

            while (! $retrievedAllMembers) {

                $rangeTop=$rangeBottom + 1499               

               ##this is how it would show up in AD
                $memberRange="member;range=$rangeBottom-$rangeTop"  

                $searcher.PropertiesToLoad.Clear()
                [void]$searcher.PropertiesToLoad.Add("$memberRange")

                $rangeBottom+=1500

                try {
                    ## should cause and exception if the $memberRange is not valid
                    $result = $searcher.FindOne() 
                    $rangedProperty = $result.Properties.PropertyNames -like "member;range=*"
                    $members +=$result.Properties.item($rangedProperty)          
                   
                     # UPDATE - 2013-03-24 check for empty group
                      if ($members.count -eq 0) { $retrievedAllMembers=$true }
                }

                catch {

                    $retrievedAllMembers=$true   ## we received all members
                }

            }

        }

        $searcher.Dispose()
        return $members

    }
    return $false   
}

2012-10-29

Can I use PowerShell functions before defining them

Yes and NO!

As I was learning syntax and intricacies of PowerShell, one of the first things I was told was that if I wanted to use a function, I had to define it first, which meant that I had to structure my script so that functions would be on top and the main script at the bottom. Obvious reason is that PowerShell parses the script sequentially starting from top, working its way to the bottom. This fact remains the same.

If you were always using PowerShell ISE and were not calling your script from the command line, this behavior would come as a surprise to you, because PowerShell ISE does not care where in the script the function is located. There is a dated but still a good intro to functions at this PowerShellPro.com link that mentions this but does not clarify the point. You can see that in the comments.

So, should you define your functions before using them? Sure! Do not bet against command line.