Link to home
Start Free TrialLog in
Avatar of Rich Rumble
Rich RumbleFlag for United States of America

asked on

speed up group membership

For reference: https://www.experts-exchange.com/questions/24131449/Speed-up-query.html
That script looks up all computers in a domain, starts to ping them one by one, once they reply back, it connects and dumps the users in the local admin group, and if you use the very last script with the -r it will look up the users that make up the groups that are listed in the local admins. Omitting the -r will look up just the user names and group names of the local admins, and not recursively break down the members of the group names.

I'd like to speed the process up, as I have large domains with thousands of computers in each.
Option Explicit
 
Const ADS_SCOPE_SUBTREE = 2
 
Sub GetMembers(strADSPath)
  ' Recursive subroutine to return group members
 
  On Error Resume Next : Err.Clear
  Dim objGroup : Set objGroup = GetObject(strADSPath)
  If Err.Number = 0 Then 
    Dim objMember
    For Each objMember in objGroup.Members
      If Not objInfLoopPrevention.Exists(objMember.ADSPath) Then
        objInfLoopPrevention.Add objMember.ADSPath, ""
        If objMember.Class = "Group" And booEnableRecursion = True Then
          GetMembers(objMember.ADSPath)
        Else
          WScript.Echo Replace(objMember.ADSPath, "WinNT://" , "")
        End If
      End If
    Next
  End If
  On Error Goto 0
 
  Set objGroup = Nothing
End Sub
 
Dim booEnableRecursion : booEnableRecursion = False
Dim objArgs : Set objArgs = WScript.Arguments
Dim strArg
For Each strArg in objArgs
  If LCase(strArg) = "-r" Then
    booEnableRecursion = True
  End If
Next
Set objArgs = Nothing
 
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
 
Dim objCommand : Set objCommand =   CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
 
objCommand.CommandText = _
  "Select Name From 'LDAP://DC=some,DC=company,DC=com' WHERE objectClass='computer'"
 
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
 
Dim objRecordSet : Set objRecordSet = objCommand.Execute
 
WScript.Echo "Number of Computers: " & objRecordSet.RecordCount
 
Do Until objRecordSet.EOF
  Dim strComputer : strComputer = objRecordSet.Fields("Name").Value
 WScript.Echo strComputer
  Dim objShell : Set objShell = CreateObject("WScript.Shell")
  Dim strCommand : strCommand = "%comspec% /c ping -n 3 -w 1000 " & strComputer & ""
  Dim objExecObject : Set objExecObject = objShell.Exec(strCommand)
 
  Do While Not objExecObject.StdOut.AtEndOfStream
    Dim strText : strText = objExecObject.StdOut.ReadAll()
    If Instr(strText, "Reply") > 0 Then
      ' =====================================================================
      ' Insert your code here
      ' =====================================================================
 
      On Error Resume Next : Err.Clear
      Dim objWMIService : Set objWMIService = GetObject _
        ("winmgmts:\\" & strComputer & "\root\cimv2")
      Dim colItems : Set colItems = objWMIService.ExecQuery _
        ("Select * From Win32_OperatingSystem")
      If Err.Number <> 0 Then
        WScript.Echo strComputer & ": Error connecting to WMI"
      Else
        Dim objItem
        For Each objItem in ColItems
          Wscript.Echo strComputer & ": " & objItem.Caption
        Next
      End If
      On Error Goto 0
 
      Dim objInfLoopPrevention : Set objInfLoopPrevention = CreateObject("Scripting.Dictionary")
 
      WScript.Echo "Local Administrators:"
      GetMembers("WinNT://" & strComputer & "/Administrators,group")
 
      Set objInfLoopPrevention = Nothing
 
      ' =====================================================================
      ' End
      ' =====================================================================
    Else
      Wscript.Echo strComputer & " could not be reached."
    End If
  Loop
  objRecordSet.MoveNext
Loop

Open in new window

Avatar of RobSampson
RobSampson
Flag of Australia image

Hey rich,

I think the only way to speed up such a sequential script like this (which look likes I wrote it...:-)), is to change it to accept a bunch of computer names at one time, so that it runs sequentially against say, 10 machines.  Then, you'd create a "parent" script that would enumerate AD, (lines 38 to 51), and call the "child" script with each 10 PCs or so.....

This way, you run multiple processes of the same script at the same time, each querying 10 different PCs.

Do you need assistance with creating a script to do that?

Regards,

Rob.

I figure at this stage Rob has more of an interest in the threading in VbScript than I. Do you have code for it already Rob? I spend way too much time playing with PowerShell / C# these days.

> which look likes I wrote it...:-)

The bits that don't belong to MS are mine, but I always found your coding style to be close to my own ;)

Chris
Avatar of Rich Rumble

ASKER

Right, that's what I thought too, and in addition, if I wanted to look up the members of groups that belong to the admin's, I thought if you did the recursion once of the entire domain, wrote it to a file, then you don't have to go back to AD each time you need to look it up.
I basically outlined all those same points in the last question Chris was good enough to assist with:
https://www.experts-exchange.com/questions/24131449/Speed-up-query.html
-rich

Depending on the functional level and operating systems on the domain controllers you don't actually have to recurse at all. MS gave use an OID we can use to return all members of a given group, including all nesting.

That doesn't help us with the WinNT provider. But I'd still split it into stages.

1. Get local group members and compile a report (means you have to wait until the end to get the results.. but..)
2. For each unique domain group, get the members.

That does mean we have to search the domain for each group we find, but at least we can limit it to only those that are really necessary.

Chris

This will need a bit of modification, we'll need an instance of ADODB.Command if you're to enable Paging and therefore return more than 1000 results.

Chris
' This bit will need the LDAP distinguished name of the group, conversion
' from the WinNT ADSPath can be done with NameTranslate.
Dim strFilter : strFilter = "(memberOf:1.2.840.113556.1.4.1941:=" & strGroupDN & ")"
 
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
 
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")
Dim objRecordSet : Set objRecordSet = objConnection.Execute( _
  "<LDAP://" & objRootDSE.Get("defaultNamingContext") & ">;" & _
  strFilter & ";" & "name;subtree")
Set objRootDSE = Nothing
 
Do Until objRecordSet.EOF
  ' Echo the name of each member
  WScript.Echo objRecordSet.Fields("name").Value
 
  objRecordSet.MoveNext
Loop

Open in new window

Hey Chris....you're right...on closer inspection, it's not my code, although we do have very similar coding styles....I probably learnt from you! LOL!

Anyway, I haven't even looked at your revisions Chris, but perhaps you could modify my new code to be a bit faster, if required....

So, what I've got is ScriptA.vbs, which calls ScriptB, with something like:
cscript <scriptB.vbs> /r:n /c:PC1;PC2;PC3

Here is script1.vbs, with these lines you can change:

Const intMaxComputers = 10
strScriptToRun = "\\server\share\GetLocalAdminGroupMembers.vbs"
' boolRecurse can be "y" or "n"
boolRecurse = "n"

Also note that currently this will only run against one "batch" of computers, with the amount of computers "per batch" defined by intMaxComputers

The reason it will only run against one batch is because of this line:
            objRecordSet.MoveLast
at line 59.

When you want it to run against all computers, in batches, comment that line out.

What it should then do is concurrently run quite a few other scripts of ScriptB.vbs

Oh, and the child threads will open in command prompts that stay open.  To change this so they close when finished, change the two
            strCommand = "cmd /k cscript.exe " & strScriptToRun & " /r:" & boolRecurse & " /c:" & strArgs

lines to
            strCommand = "cmd /c cscript.exe " & strScriptToRun & " /r:" & boolRecurse & " /c:" & strArgs


I guess, in ScriptA.vbs, you could restrict the LDAP query to a sub OU so that you control how many PCs it runs against...

Anyway, see below for ScriptA.vbs....ScriptB.vbs to follow....

Regards,

Rob.
If LCase(Right(Wscript.FullName, 11)) = "wscript.exe" Then
    strPath = Wscript.ScriptFullName
    strCommand = "%comspec% /k cscript  """ & strPath & """"
    Set objShell = CreateObject("Wscript.Shell")
    objShell.Run(strCommand), 1, True
    Wscript.Quit
End If
 
Set objShell = CreateObject("WScript.Shell")
Dim strScriptToRun
Set objFSO = CreateObject("Scripting.FileSystemObject")
 
Const intMaxComputers = 10
strScriptToRun = "\\server\share\GetLocalAdminGroupMembers.vbs"
' boolRecurse can be "y" or "n"
boolRecurse = "n"
 
strScriptToRun = objFSO.GetFile(strScriptToRun).ShortPath
 
Const ADS_SCOPE_SUBTREE = 2
 
Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
objConnection.Provider = "ADsDSOObject"
objConnection.Open "Active Directory Provider"
 
Dim objCommand : Set objCommand =   CreateObject("ADODB.Command")
Set objCommand.ActiveConnection = objConnection
 
Dim objRootDSE, strDNSDomain
Set objRootDSE = GetObject("LDAP://RootDSE")
strDNSDomain = objRootDSE.Get("defaultNamingContext")
 
objCommand.CommandText = _
  "Select Name From 'LDAP://" & strDNSDomain & "' WHERE objectClass='computer'"
 
objCommand.Properties("Page Size") = 1000
objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
 
Dim objRecordSet : Set objRecordSet = objCommand.Execute
 
WScript.Echo "Number of Computers: " & objRecordSet.RecordCount
 
intCompCount = 0
strArgs = ""
While Not objRecordSet.EOF
	If intCompCount < intMaxComputers Then
		If strArgs = "" Then
			strArgs = objRecordSet.Fields("Name").Value
		Else
			strArgs = strArgs & ";" & objRecordSet.Fields("Name").Value
		End If
		intCompCount = intCompCount + 1
	Else
		strCommand = "cmd /k cscript.exe " & strScriptToRun & " /r:" & boolRecurse & " /c:" & strArgs
		'WScript.Echo "Running " & strCommand
		objShell.Run strCommand, 1, False
		strArgs = ""
		intCompCount = 0
		objRecordSet.MoveLast
	End If
	objRecordSet.MoveNext
Wend
If intCompCount > 0 Then
		strCommand = "cmd /k cscript.exe " & strScriptToRun & " /r:" & boolRecurse & " /c:" & strArgs
		'WScript.Echo "Running " & strCommand
		objShell.Run strCommand, 1, False
End If
WScript.Echo VbCrLf & "Finished."

Open in new window

And here's the second "child" script....no modifications required....

Regards,

Rob.
Option Explicit
 
Dim booEnableRecursion : booEnableRecursion = False
If WScript.Arguments.Named("r") = "" Or (WScript.Arguments.Named("r") <> "n" And WScript.Arguments.Named("r") <> "y") Then
	booEnableRecursion = False
ElseIf WScript.Arguments.Named("r") = "y" Then
	booEnableRecursion = True
End If
 
If WScript.Arguments.Named("c") = "" Then
	WScript.Echo "No computers were passed to the script."
Else
	Dim arrComputers, strComputer
	arrComputers = Split(WScript.Arguments.Named("c"), ";")
	WScript.Echo "Number of Computers: " & UBound(arrComputers) + 1
	For Each strComputer In arrComputers
		WScript.Echo strComputer
		Dim objShell : Set objShell = CreateObject("WScript.Shell")
		If Ping(strComputer) = True Then
			' =====================================================================
			' Insert your code here
			' =====================================================================
			On Error Resume Next : Err.Clear
			Dim objWMIService : Set objWMIService = GetObject _
				("winmgmts:\\" & strComputer & "\root\cimv2")
			Dim colItems : Set colItems = objWMIService.ExecQuery _
				("Select * From Win32_OperatingSystem")
			If Err.Number <> 0 Then
				WScript.Echo strComputer & ": Error connecting to WMI"
			Else
				Dim objItem
				For Each objItem in colItems
					Wscript.Echo strComputer & ": " & objItem.Caption
				Next
			End If
			On Error Goto 0
	
			Dim objInfLoopPrevention : Set objInfLoopPrevention = CreateObject("Scripting.Dictionary")
	
			WScript.Echo "Local Administrators:"
			GetMembers("WinNT://" & strComputer & "/Administrators,group")
	
			Set objInfLoopPrevention = Nothing
	
			' =====================================================================
			' End
			' =====================================================================
		Else
			Wscript.Echo strComputer & " could not be reached."
		End If
	Next
End If
 
Sub GetMembers(strADSPath)
	' Recursive subroutine to return group members
	On Error Resume Next : Err.Clear
	Dim objGroup : Set objGroup = GetObject(strADSPath)
	If Err.Number = 0 Then 
		Dim objMember
		For Each objMember in objGroup.Members
			If Not objInfLoopPrevention.Exists(objMember.ADSPath) Then
				objInfLoopPrevention.Add objMember.ADSPath, ""
				If objMember.Class = "Group" And booEnableRecursion = True Then
					GetMembers(objMember.ADSPath)
				Else
					WScript.Echo Replace(objMember.ADSPath, "WinNT://" , "")
				End If
			End If
		Next
	End If
	On Error Goto 0
	Set objGroup = Nothing
End Sub
 
Function Ping(strComputer)
	Dim objShell, boolCode
	Set objShell = CreateObject("WScript.Shell")
	boolCode = objShell.Run("Ping -n 3 -w 1000 " & strComputer, 0, True)
	If boolCode = 0 Then
		Ping = True
	Else
		Ping = False
	End If
End Function

Open in new window


I think we need a few more things... I'll have a think about them and post code...

1. A common source to write the responses back to.

At the moment we're going to be spawning windows all over the place and dropping output into each spawned window. Solutions to this include an SQL database, or a collection of flat-files that can be re-assembled by the master script after execution completes.

2. A thread limiter.

If each batch is 10 computers, a search for 1000 computers will spawn 100 VbScript threads at (almost) the same time.

I was thinking of a pool approach where 10 or so threads are available for processing. Potentially achievable if either we monitor Win32_Process or wait for a file to appear signalling the end of the script.

That does slow it down, but my last large domain consisted of 4500 computers in the domain (9000 in the forest). That would give us quite a scary number of threads trying to execute.

Chris

Almost time to play foosball ;) Figure I'll post what I have so far so you can have a look through then I'll come back to it later on.

This implementation of threading requires the child script to return a numbered file (e.g. 1.results) to signal the end of the child script. At the moment my child script consists of this, purely to test thread management (because the threads don't terminate sequentially):

Set objArgs = WScript.Arguments
strThreadNumber = objArgs(1)

Randomize()
i = Int(48 * Rnd())

WScript.Sleep (i * 1000)

Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Set objFile = objFileSystem.OpenTextFile(strThreadNumber & ".results", 2, True, 0)

The main script passes a semi-colon delimited list of computers and a numeric thread identifier when starting the thread:

objShell.Run "ChildScript.vbs " & arrBatches(i) & " " & i, 0, False

I split up all the processing and batch generation, didn't like holding onto the connection to AD for so long (and it would be a very long time).

Thoughts are always welcome.

The "To Do" list includes:

* Compilation of return which will be held in the .results file (when I decide on a format for that)
* Returning membership for all unique domain groups found in the result set

Chris
Option Explicit
 
' Script Constants
 
' Maximum number of computers to add into each thread
Const MAX_PER_THREAD = 10
' Maximum number of threads to execute
Const MAX_THREADS = 10
 
' Functions
 
Function GetComputersFromAD(strPort, strBaseDN)
  ' Returns an array containing all computers in AD. Performed here so we do not have to maintain 
  ' a connection to AD for any longer than necessary. Valid Ports are LDAP or GC.
 
  Const ADS_SCOPE_SUBTREE = 2
 
  Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
  objConnection.Provider = "ADsDSOObject"
  objConnection.Open "Active Directory Provider"
 
  Dim objCommand : Set objCommand =   CreateObject("ADODB.Command")
  Set objCommand.ActiveConnection = objConnection
 
  objCommand.CommandText = "SELECT name FROM '" & strPort & "://" & strBaseDN & _
    "' WHERE objectClass='computer'"
 
  objCommand.Properties("Page Size") = 1000
  objCommand.Properties("Searchscope") = ADS_SCOPE_SUBTREE
 
  Dim objRecordSet : Set objRecordSet = objCommand.Execute
 
  Dim arrComputers()
  Dim i : i = 0
 
  Do Until objRecordSet.EOF
    ReDim Preserve arrComputers(i)
    arrComputers(i) = objRecordSet.Fields("name").Value
    i = i + 1
    objRecordSet.MoveNext
  Loop
 
  Set objRecordSet = Nothing
  Set objCommand = Nothing
  Set objConnection = Nothing
 
  GetComputersFromAD = arrComputers
End Function
 
Function BatchGenerator(arrComputers)
  Dim intTotalComputers : intTotalComputers = UBound(arrComputers) + 1
  Dim intPerThread : intPerThread = MAX_PER_THREAD
 
  ' Reduce the maximum number per thread if it will leave threads in the pool doing nothing
  If (intTotalComputers / MAX_THREADS) < MAX_PER_THREAD Then
    intPerThread = Round(intTotalComputers / MAX_THREADS)
  End If
 
  Dim intCurrentBatchCount : intCurrentBatchCount = 0
  Dim intBatchNo : intBatchNo = 0
  Dim arrBatches() : ReDim arrBatches(intBatchNo)
  Dim strComputer
  For Each strComputer in arrComputers
    If intCurrentBatchCount = intPerThread Then
      intCurrentBatchCount = 0
      intBatchNo = intBatchNo + 1
      ReDim Preserve arrBatches(intBatchNo)
    End If
 
    intCurrentBatchCount = intCurrentBatchCount + 1
    arrBatches(intBatchNo) = arrBatches(intBatchNo) & strComputer & ";"
  Next
  BatchGenerator = arrBatches
End Function
 
Sub ThreadManager(arrBatches)
  Dim objShell : Set objShell = CreateObject("WScript.Shell")
 
  Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
 
  Dim objFolder, objFile
  Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
  For Each objFile in objFolder.Files
    If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
      objFile.Delete
    End If
  Next
 
  Dim objThreadMonitor : Set objThreadMonitor = CreateObject("Scripting.Dictionary")
 
  Dim i : i = 0
  Do Until i = UBound(arrBatches)
 
    ' Start Threads if Threads can be started
    If objThreadMonitor.Count < MAX_THREADS Then
      ' WScript.Echo "Starting " & i
      objThreadMonitor.Add i, ""
      objShell.Run "ChildScript.vbs " & arrBatches(i) & " " & i, 0, False
      i = i + 1
    Else
      ' WScript.Echo "Sleeping for 10 seconds"
      WScript.Sleep 10000
    End If
 
    ' Look for Finished Threads
    Dim j
    For Each j in objThreadMonitor
      If objFileSystem.FileExists(j & ".results") Then
        ' WScript.Echo "Stopping " & j
        objThreadMonitor.Remove j
      End If
    Next
  Loop
 
  ' Wait for completion of threads
  Do Until objThreadMonitor.Count = 0
    ' WScript.Echo "Batches complete. Waiting for Thread Shutdown"
    WScript.Sleep 10000
 
    For Each j in objThreadMonitor
      If objFileSystem.FileExists(j & ".results") Then
        ' WScript.Echo "Stopping " & j
        objThreadMonitor.Remove j
      End If
    Next
  Loop
End Sub
 
'
' Main Code
'
 
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")
 
' Alternatives include GC and rootDomainNamingContext
Dim arrComputers : arrComputers = GetComputersFromAD( _
  "LDAP", _
  objRootDSE.Get("defaultNamingContext"))
 
Set objRootDSE = Nothing
 
Dim arrBatches : arrBatches = BatchGenerator(arrComputers)
 
ThreadManager arrBatches

Open in new window


This is my version of the child script. It gives us text files that look something like this:

<ComputerName>|<OperatingSystem>|<Group>|<Members>

Group is formatted like this:

GroupName#GroupADSPath

The latter for the "just in case" / "while I'm here" type reasoning. Members is formatted like this:

Member;Member;Member

Then Member itself into this:

Name#ADSPath#Class

Confused yet?

I removed recursion, instead it enumerates all local groups (harkens back to my original script). Doing this means we don't have to worry about identifying whether something is a Domain Group or not (at this stage).

I have a modification to the main script (above) that parses the file returned by this script. Using that we can filter through all the members that are Groups, and figure out if we need to grab membership for the domain (after we create a unique list of Domain groups from the local groups).

Chris
Option Explicit
 
Function GetMembers(strADSPath)
  ' Returns members as composite string
 
  Dim objGroup : Set objGroup = GetObject(strADSPath)
  Dim objMember
  Dim strMembers : strMembers = ""
  For Each objMember in objGroup.Members
    Dim strMember : strMember = objMember.Name & "#" & objMember.ADSPath & "#" & objMember.Class
 
    strMembers = strMembers & strMember & ";"
  Next
 
  Set objGroup = Nothing
 
  GetMembers = strMembers
End Function
 
Function Ping(strComputer)
  Dim objShell : Set objShell = CreateObject("WScript.Shell")
  Dim booCode : booCode = objShell.Run("Ping -n 3 -w 1000 " & strComputer, 0, True)
  If booCode = 0 Then
    Ping = True
  Else
    Ping = False
  End If
End Function
 
'
' Main Code
'
 
Dim objArgs : Set objArgs = WScript.Arguments
Dim strComputers : strComputers = objArgs(0)
Dim strThreadNumber : strThreadNumber = objArgs(1)
 
Dim arrComputers : arrComputers = Split(strComputers, ";")
Dim strOutput
 
Dim strComputer
For Each strComputer In arrComputers
  If Ping(strComputer) = True Then
 
    On Error Resume Next : Err.Clear
    Dim objWMIService : Set objWMIService = GetObject("winmgmts://" & strComputer & "/root/cimv2")
 
    Dim strOS : strOS = ""
    If Err.Number = 0 Then
      Dim colItems : Set colItems = objWMIService.ExecQuery _
        ("SELECT Caption FROM Win32_OperatingSystem")
      Dim objItem
      For Each objItem in colItems
        strOS = objItem.Caption
      Next
    End If
    On Error Goto 0
 
    Dim objLocalSAM : Set objLocalSAM = GetObject("WinNT://" & strComputer)
    objLocalSAM.Filter = Array("Group")
    Dim objGroup
    For Each objGroup in objLocalSAM
      Dim strMembers : strMembers = GetMembers(objGroup.ADSPath)
 
      strOutput = strOutput & strComputer & "|" & strOS & "|" & _
        objGroup.Name & "#" & objGroup.ADSPath & "|" & strMembers & vbCrLf
    Next
  End If
Next
 
Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
Dim objFile : Set objFile = objFileSystem.OpenTextFile(strThreadNumber & ".results", 2, True, 0)
 
objFile.Write strOutput
 
Set objFile = Nothing
Set objFileSystem = Nothing

Open in new window


Well... it's a bit of a monstrous script now, but I'm done.

It does this...

1. Gets a list of computers from AD (excluding Domain Controllers)
2. Splits those computers into batches for multi-threaded execution
3. Gets all local groups for all computers returned (if it can)
4. Pulls together all the results from the separate files
5. Extracts a list of Domain Groups from the results
6. Gets the full membership of the group in the domain
7. Writes the results to the command line, replacing Domain Groups with their members

I've tested most of it, but I haven't allowed it to do a full run on my network (I lock down access to WMI, and my PC does not have access to that, therefore every connection is slow as hell).

Naturally you should feel free to increase the thread pool size if you wish (MAX_THREADS).

Going to make myself a nice relaxing cup of tea now.

Chris
Option Explicit
 
' Script Constants
 
' Maximum number of computers to add into each thread
Const MAX_PER_THREAD = 10
' Maximum number of threads to execute
Const MAX_THREADS = 10
 
' Functions
 
Function GetComputersFromAD(strPort, strBaseDN)
  ' Returns an array containing all computers in AD. Performed here so we do not have to maintain 
  ' a connection to AD for any longer than necessary. Valid Ports are LDAP or GC.
 
  Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
  objConnection.Provider = "ADsDSOObject"
  objConnection.Open "Active Directory Provider"
 
  Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
  Set objCommand.ActiveConnection = objConnection
  objCommand.Properties("Page Size") = 1000
 
  Dim strFilter : strFilter = "(&(objectClass=computer)(!frscomputerreferencebl=*))"
  objCommand.CommandText = "<" & strPort & "://" & strBaseDN & ">;" & strFilter & ";" & _
    "name;subtree"
  
  Dim objRecordSet : Set objRecordSet = objCommand.Execute
 
  Dim arrComputers()
  Dim i : i = 0
 
  Do Until objRecordSet.EOF
    ReDim Preserve arrComputers(i)
    arrComputers(i) = objRecordSet.Fields("name").Value
    i = i + 1
    objRecordSet.MoveNext
  Loop
 
  Set objRecordSet = Nothing
  Set objCommand = Nothing
  Set objConnection = Nothing
 
  GetComputersFromAD = arrComputers
End Function
 
Function BatchGenerator(arrComputers)
  Dim intTotalComputers : intTotalComputers = UBound(arrComputers) + 1
  Dim intPerThread : intPerThread = MAX_PER_THREAD
 
  ' Reduce the maximum number per thread if it will leave threads in the pool doing nothing
  If (intTotalComputers / MAX_THREADS) < MAX_PER_THREAD Then
    intPerThread = Round(intTotalComputers / MAX_THREADS)
  End If
 
  Dim intCurrentBatchCount : intCurrentBatchCount = 0
  Dim intBatchNo : intBatchNo = 0
  Dim arrBatches() : ReDim arrBatches(intBatchNo)
  Dim strComputer
  For Each strComputer in arrComputers
    If intCurrentBatchCount = intPerThread Then
      intCurrentBatchCount = 0
      intBatchNo = intBatchNo + 1
      ReDim Preserve arrBatches(intBatchNo)
    End If
 
    intCurrentBatchCount = intCurrentBatchCount + 1
    arrBatches(intBatchNo) = arrBatches(intBatchNo) & strComputer & ";"
  Next
  BatchGenerator = arrBatches
End Function
 
Sub ThreadManager(arrBatches)
  Dim objShell : Set objShell = CreateObject("WScript.Shell")
 
  Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
 
  Dim objFolder, objFile
  Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
  For Each objFile in objFolder.Files
    If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
      objFile.Delete
    End If
  Next
 
  Dim objThreadMonitor : Set objThreadMonitor = CreateObject("Scripting.Dictionary")
 
  Dim i : i = 0
  Do Until i = UBound(arrBatches)
 
    ' Start Threads if Threads can be started
    If objThreadMonitor.Count < MAX_THREADS Then
      ' WScript.Echo "Starting " & i
      objThreadMonitor.Add i, ""
      objShell.Run "cscript.exe ChildScript.vbs " & arrBatches(i) & " " & i, 0, False
      i = i + 1
    Else
      ' WScript.Echo "Sleeping for 10 seconds"
      WScript.Sleep 10000
    End If
 
    ' Look for Finished Threads
    Dim j
    For Each j in objThreadMonitor
      If objFileSystem.FileExists(j & ".results") Then
        ' WScript.Echo "Stopping " & j
        objThreadMonitor.Remove j
      End If
    Next
  Loop
 
  ' Wait for completion of remaining threads
  Do Until objThreadMonitor.Count = 0
    ' WScript.Echo "Batches complete. Waiting for Thread Shutdown"
    WScript.Sleep 10000
 
    For Each j in objThreadMonitor
      If objFileSystem.FileExists(j & ".results") Then
        ' WScript.Echo "Stopping " & j
        objThreadMonitor.Remove j
      End If
    Next
  Loop
 
  Set objFileSystem = Nothing
  Set objThreadMonitor = Nothing
  Set objShell = Nothing
End Sub
 
Function CollateResultFiles
  Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
  Dim objFolder : Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
 
  Dim objLocalGroups : Set objLocalGroups = CreateObject("Scripting.Dictionary")
  objLocalGroups.CompareMode = VbTextCompare
 
  Dim objFile
  For Each objFile in objFolder.Files
    If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
      Dim objStream : Set objStream = objFile.OpenAsTextStream(1, 0)
 
      Do Until objStream.AtEndOfStream
        Dim arrLine : arrLine = Split(objStream.ReadLine, "|")
        Dim strComputer : strComputer = arrLine(0)
        Dim strOS : strOS = arrLine(1)
        Dim strGroup : strGroup = arrLine(2)
        Dim strMembers : strMembers = arrLine(3)
 
        objLocalGroups.Add strComputer & "/" & strGroup, Array(strOS, strMembers)
      Loop
 
      Set objStream = Nothing
    End If
  Next
 
  CollateResultFiles = objLocalGroups
End Function
 
Function GetDomainGroups(objLocalGroups)
  ' Create a list of domain group names from the data returned
 
  Dim objDomainGroups : Set objDomainGroups = CreateObject("Scripting.Dictionary")
 
  Dim strLocalGroups
  For Each strLocalGroup in objLocalGroups
    Dim strMembers : strMembers = objLocalGroups(strLocalGroup)(1)
 
    If strMembers <> "" Then
      Dim arrMembers : arrMembers = Split(strMembers, ";")
 
      Dim i
      For i = 0 To (UBound(arrMembers) - 1)
        Dim arrMember : arrMember = Split(arrMembers(i), "#")
 
        If UBound(Split(arrMember(1), "/")) = 3 _
            And InStr(arrMember(1), "NT AUTHORITY") = 0 _
            And arrMember(2) = "Group" Then
 
          Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
          If Not objDomainGroups.Exists(strDomainGroup) Then
            objDomainGroups.Add strDomainGroup, ""
          End If
        End If
      Next
    End If
  Next
  Set GetDomainGroups = objDomainGroups
End Function
 
Function ExpandDomainGroups(objDomainGroups, strBaseDN)
  ' This method requires Windows 2003 SP1 minimum
 
  Dim strDomainGroup
  For Each strDomainGroup in objDomainGroups
    Dim objGroupDN : strGroupDN = GetObjectDN(strDomainGroup)
 
    Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
    objConnection.Provider = "ADsDSOObject"
    objConnection.Open "Active Directory Provider"
 
    Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
    Set objCommand.ActiveConnection = objConnection
    objCommand.Properties("Page Size") = 1000
 
    Dim strFilter : strFilter = _
      "(&(objectClass=user)(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=" & strGroupDN & "))"
    objCommand.CommandText = "<LDAP://" & strBaseDN & ">;" & strFilter & ";" & _
      "aDSPath,sAMAccountName;subtree"
   
    Dim objRecordSet : Set objRecordSet = objCommand.Execute
 
    Dim strMembers : strMembers = ""
    Do Until objRecordSet.EOF
      strMembers = strMembers & objRecordSet.Fields("sAMAccountName").Value & _
        "#" & objRecordSet.Fields("aDSPAth") & "#User;"
 
      objDomainGroups(strDomainGroup) = strMembers
 
      objRecordSet.MoveNext
    Loop
 
    Set objRecordSet = Nothing
    Set objCommand = Nothing
    Set objConnection = Nothing
  Next
 
  Set ExpandDomainGroups = objDomainGroups
End Function
 
Function GetObjectDN(strObject)
  ' Return Type: String
  '
  ' Returns a Distinguished Name for an Object from it's NT SAM ID.
  ' This will only function for valid object types within an NT Domain structure.
 
  Const ADS_NAME_INITTYPE_GC = 3
  Const ADS_NAME_TYPE_1779 = 1
  Const ADS_NAME_TYPE_NT4 = 3
 
  On Error Resume Next : Err.Clear
  Dim objNameTranslate : Set objNameTranslate = CreateObject("NameTranslate")
 
  objNameTranslate.Init ADS_NAME_INITTYPE_GC, ""
  objNameTranslate.Set ADS_NAME_TYPE_NT4, strObject
  Dim strObjectDN : strObjectDN = objNameTranslate.Get(ADS_NAME_TYPE_1779)
  If Err.Number <> 0 Then  ' Make the DN Blank for a Failed Search
    strObjectDN = ""
  End If
 
  Set objNameTranslate = Nothing
  On Error Goto 0
 
  GetObjectDN = strObjectDN
End Function
 
Sub WriteResults(objLocalGroups, objDomainGroups)
 
  Dim strLocalGroups
  For Each strLocalGroup in objLocalGroups
    WScript.Echo strLocalGroup & " - " & objLocalGroups(strLocalGroup)(0)
    DisplayMembers strMembers, objDomainGroups
    WScript.Echo
  Next
End Sub
 
Sub DisplayMembers(strMembers, objDomainGroups)
  If strMembers <> "" Then
    Dim arrMembers : arrMembers = Split(strMembers, ";")
 
    Dim i
    For i = 0 To (UBound(arrMembers) - 1)
      Dim strMember : strMember = arrMembers(i)
      Dim arrMember : arrMember = Split(strMember, "#")
 
      If UBound(Split(arrMember(1), "/")) = 3 _
          And InStr(arrMember(1), "NT AUTHORITY") = 0 _
          And arrMember(2) = "Group" Then
 
        Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
        If objDomainGroups.Exists(strDomainGroup) Then
          strMembers = Replace(strMembers, strMember & ";", objDomainGroups(strDomainGroup))
        End If
      End If
    Next
 
    arrMembers = Split(strMembers, ";")
    For i = 0 To (UBound(arrMembers) - 1)
      arrMember = Split(arrMembers(i), "#")
      WScript.Echo arrMember(0)
    Next
  End If
End Sub
 
'
' Main Code
'
 
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")
Dim strDomainDN : strDomainDN = objRootDSE.Get("defaultNamingContext")
Set objRootDSE = Nothing
 
' Alternatives include GC and rootDomainNamingContext
Dim arrComputers : arrComputers = GetComputersFromAD("LDAP", strDomainDN)
 
Dim arrBatches : arrBatches = BatchGenerator(arrComputers)
 
ThreadManager arrBatches
 
Dim objLocalGroups : Set objLocalGroups = CollateResultFiles
Dim objDomainGroups : Set objDomainGroups = GetDomainGroups(objLocalGroups)
Set objDomainGroups = ExpandDomainGroups(objDomainGroups, strDomainDN)
 
WriteResults objLocalGroups, objDomainGroups

Open in new window

I'm giving it a try now, I'll see if I can shoe-horn the ping script in... You've really outdone yourself... that's a lot of code!
-rich

Ping is in the child script already, it's designed to run with the one I posted above :)

Chris
Ahh, I missed that. So ID: 23632695 and ID: 23633665 need to be in the same folder(location) on the hd... do they need to named a certain way or called a certain way
cscript /nologo scriptA.vbs ?? and scriptA will call scriptB.vbs on it's own?
I see ChildScript.vbs in the code.
-rich

Yep, they will. The two must be in the same folder unless we go in and configure both scripts to use specific folders for the reports.

For the main script it doesn't matter what it's called.

The child only matters if it's named than in the main script (so either stick with ChildScript.vbs or modify that in the main (parent) script).

Chris

missing words... good sign, right? ;)

 > named than

... named differently than ...

Chris
The results files are building up, and so far they look good, can't wait until the loop finishes, I've got 10 spawned cscript's. I think these techniques could be utilized for a lot more big searches like this. It'd be neat to have a basic template, like the M$ ping script I was using, and a place inside one of the scripts to put ones own code. I realize with this one, there is recursion and other specialized portions, but I wonder if a generic version could be derived.
Nevermind... if I start thinking about scope creep, this question would never get done ;)
I think there are a few too many variables to consider, but if the goal is to do XYZ to all domain computers, then maybe a generic version of this could be done? Don't to it here unless you feel compelled, I''ll make yet another question if anyone was willing to participate. I can already tell this is going much faster. I have to audit share permissions pretty soon so I'd like to apply the same principals to that too :)
-rich

> can't wait until the loop finishes

I suspect you'll find the output format leaves a lot to be desired. I'm far from keen on it, too much data to display. Fortunately... we can always re-parse the .result files without re-running the script. In fact it's important not to re-run the script if you want to do that, because the first thing ThreadManager does is purge the existing result files.

> I think these techniques could be utilized for a lot more big searches like this

WMI perhaps. Anything that hooks into a large client base and must wait for a return in each instance.

If whatever we're doing is responding quickly there's no benefit. Ultimately it'll all bottleneck on the CPU on the local machine.

> and a place inside one of the scripts to put ones own code

Rip out the final functions that deal with collating the text files and formatting output and you can. Simply replace the Child Script with one that does what you need for the passed group of computers.

Still, I'd say this task, retrieving membership like this, is complex. As such it's very difficult to make generic scripts, at least in any way above the basic functions (many of which I use time and time again).

For the share permissions, it won't have anything like this scope will it? Or where you looking to grab those for every host on the domain as well?

Chris
Right, every host on the domain for the shares. I have scanning tools that can do it, but I like "rolling" my own as it were. Not that I have done any work whatsoever. I'm about 1/2 way through, 150 results files and if my math holds up, 8-10 hosts in each file, I'm almost exactly 1/2 way.

I'm also fascinated with the whole process, the way programmers are, it's great, I'm glad guys like you are around. This share idea goes back to another one you knocked out:
https://www.experts-exchange.com/questions/21986624/Combine-these-two-WSH-scripts.html

As far as output, your right, I like that one could use those results files as they see fit, being able to parse them with whatever they choose. I could of sworn there was an easy way to output csv, xml,html with vbscript, without adding "," or other tags around each variable manually. The ScriptoMatic does it, but I've looked at that hta, and it's beyone me how they do it, and how they convert to other languages too.
-rich

Ahh that one. We can use most of the code we did above for that one. The changes will be in the child script, then it's again just a case of deciding how to present the results.

I've never found anything that'll automatically format for VbScript. Entirely possible I didn't look hard enough though, I tend to get dug into the bits I like (directory services mostly) and completely ignore anything outside of that unless I really need it.

I figure that's the advantage of being a sys admin / engineer rather than a programmer, I don't have to learn it properly, only the bits I like :)

Chris
I think I found it...
http://msdn.microsoft.com/en-us/library/aa393065(VS.85).aspx#encode_an_object_using_vbscript
And if you look in scriptomaticV2.hta around line 959 is the method they use for xml. Again, fascinating :) If the output were XML it'd be easy to create a simple XSL sheet to style the ouput.
I'm sure powershell has more accessible output than vbs/wmi does. WMIC has it built in, and I could probably just reuse their xsl sheets for the most part.
-rich

Not sure we can use that here, but if XML is good we can parse the files and output XML instead?

Chris
Yeah, sure... I'm just used to doing xml or csv so *hacky*, just echo'ing the tag's before and after or echoing the comma after the variable.. Like I did with XML here:
https://www.experts-exchange.com/questions/23545465/Vbscript-needs-to-accept-alternate-username-and-password.html?anchorAnswerId=21951146#a21951146
I like the way scriptomatic does it, so it may be good to emulate them and reuse the xsl files (C:\WINDOWS\system32\wbem) in the wbem dir.
We'll keep that for another day ;)
-rich
doh
C:\admin.vbs(156, 3) Microsoft VBScript runtime error: Wrong number of arguments or invalid property assignment
...
 CollateResultFiles = objLocalGroups
End Function

I swear i didn't do anything!
-rich

Quick check in before bed time and D'oh!

Missing a Set there (Set CollateResultFiles = objLocalGroups).

I've fixed that here and commented out enough so that it'll just continue from where it is (pulling it together and displaying the output). Should work with the first three functions commented out as below. Just make a copy of the report files in case ;)

Chris
Option Explicit
 
' Script Constants
 
' Maximum number of computers to add into each thread
Const MAX_PER_THREAD = 10
' Maximum number of threads to execute
Const MAX_THREADS = 10
 
' Functions
 
Function GetComputersFromAD(strPort, strBaseDN)
  ' Returns an array containing all computers in AD. Performed here so we do not have to maintain 
  ' a connection to AD for any longer than necessary. Valid Ports are LDAP or GC.
 
  Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
  objConnection.Provider = "ADsDSOObject"
  objConnection.Open "Active Directory Provider"
 
  Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
  Set objCommand.ActiveConnection = objConnection
  objCommand.Properties("Page Size") = 1000
 
  Dim strFilter : strFilter = "(&(objectClass=computer)(!frscomputerreferencebl=*))"
  objCommand.CommandText = "<" & strPort & "://" & strBaseDN & ">;" & strFilter & ";" & _
    "name;subtree"
  
  Dim objRecordSet : Set objRecordSet = objCommand.Execute
 
  Dim arrComputers()
  Dim i : i = 0
 
  Do Until objRecordSet.EOF
    ReDim Preserve arrComputers(i)
    arrComputers(i) = objRecordSet.Fields("name").Value
    i = i + 1
    objRecordSet.MoveNext
  Loop
 
  Set objRecordSet = Nothing
  Set objCommand = Nothing
  Set objConnection = Nothing
 
  GetComputersFromAD = arrComputers
End Function
 
Function BatchGenerator(arrComputers)
  Dim intTotalComputers : intTotalComputers = UBound(arrComputers) + 1
  Dim intPerThread : intPerThread = MAX_PER_THREAD
 
  ' Reduce the maximum number per thread if it will leave threads in the pool doing nothing
  If (intTotalComputers / MAX_THREADS) < MAX_PER_THREAD Then
    intPerThread = Round(intTotalComputers / MAX_THREADS)
  End If
 
  Dim intCurrentBatchCount : intCurrentBatchCount = 0
  Dim intBatchNo : intBatchNo = 0
  Dim arrBatches() : ReDim arrBatches(intBatchNo)
  Dim strComputer
  For Each strComputer in arrComputers
    If intCurrentBatchCount = intPerThread Then
      intCurrentBatchCount = 0
      intBatchNo = intBatchNo + 1
      ReDim Preserve arrBatches(intBatchNo)
    End If
 
    intCurrentBatchCount = intCurrentBatchCount + 1
    arrBatches(intBatchNo) = arrBatches(intBatchNo) & strComputer & ";"
  Next
  BatchGenerator = arrBatches
End Function
 
Sub ThreadManager(arrBatches)
  Dim objShell : Set objShell = CreateObject("WScript.Shell")
 
  Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
 
  Dim objFolder, objFile
  Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
  For Each objFile in objFolder.Files
    If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
      objFile.Delete
    End If
  Next
 
  Dim objThreadMonitor : Set objThreadMonitor = CreateObject("Scripting.Dictionary")
 
  Dim i : i = 0
  Do Until i = UBound(arrBatches)
 
    ' Start Threads if Threads can be started
    If objThreadMonitor.Count < MAX_THREADS Then
      ' WScript.Echo "Starting " & i
      objThreadMonitor.Add i, ""
      objShell.Run "cscript.exe ChildScript.vbs " & arrBatches(i) & " " & i, 0, False
      i = i + 1
    Else
      ' WScript.Echo "Sleeping for 10 seconds"
      WScript.Sleep 10000
    End If
 
    ' Look for Finished Threads
    Dim j
    For Each j in objThreadMonitor
      If objFileSystem.FileExists(j & ".results") Then
        ' WScript.Echo "Stopping " & j
        objThreadMonitor.Remove j
      End If
    Next
  Loop
 
  ' Wait for completion of remaining threads
  Do Until objThreadMonitor.Count = 0
    ' WScript.Echo "Batches complete. Waiting for Thread Shutdown"
    WScript.Sleep 10000
 
    For Each j in objThreadMonitor
      If objFileSystem.FileExists(j & ".results") Then
        ' WScript.Echo "Stopping " & j
        objThreadMonitor.Remove j
      End If
    Next
  Loop
 
  Set objFileSystem = Nothing
  Set objThreadMonitor = Nothing
  Set objShell = Nothing
End Sub
 
Function CollateResultFiles
  Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
  Dim objFolder : Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
 
  Dim objLocalGroups : Set objLocalGroups = CreateObject("Scripting.Dictionary")
  objLocalGroups.CompareMode = VbTextCompare
 
  Dim objFile
  For Each objFile in objFolder.Files
    If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
      Dim objStream : Set objStream = objFile.OpenAsTextStream(1, 0)
 
      Do Until objStream.AtEndOfStream
        Dim arrLine : arrLine = Split(objStream.ReadLine, "|")
        Dim strComputer : strComputer = arrLine(0)
        Dim strOS : strOS = arrLine(1)
        Dim strGroup : strGroup = arrLine(2)
        Dim strMembers : strMembers = arrLine(3)
 
        objLocalGroups.Add strComputer & "/" & strGroup, Array(strOS, strMembers)
      Loop
 
      Set objStream = Nothing
    End If
  Next
 
  Set CollateResultFiles = objLocalGroups
End Function
 
Function GetDomainGroups(objLocalGroups)
  ' Create a list of domain group names from the data returned
 
  Dim objDomainGroups : Set objDomainGroups = CreateObject("Scripting.Dictionary")
 
  Dim strLocalGroups
  For Each strLocalGroup in objLocalGroups
    Dim strMembers : strMembers = objLocalGroups(strLocalGroup)(1)
 
    If strMembers <> "" Then
      Dim arrMembers : arrMembers = Split(strMembers, ";")
 
      Dim i
      For i = 0 To (UBound(arrMembers) - 1)
        Dim arrMember : arrMember = Split(arrMembers(i), "#")
 
        If UBound(Split(arrMember(1), "/")) = 3 _
            And InStr(arrMember(1), "NT AUTHORITY") = 0 _
            And arrMember(2) = "Group" Then
 
          Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
          If Not objDomainGroups.Exists(strDomainGroup) Then
            objDomainGroups.Add strDomainGroup, ""
          End If
        End If
      Next
    End If
  Next
  Set GetDomainGroups = objDomainGroups
End Function
 
Function ExpandDomainGroups(objDomainGroups, strBaseDN)
  ' This method requires Windows 2003 SP1 minimum
 
  Dim strDomainGroup
  For Each strDomainGroup in objDomainGroups
    Dim objGroupDN : strGroupDN = GetObjectDN(strDomainGroup)
 
    Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
    objConnection.Provider = "ADsDSOObject"
    objConnection.Open "Active Directory Provider"
 
    Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
    Set objCommand.ActiveConnection = objConnection
    objCommand.Properties("Page Size") = 1000
 
    Dim strFilter : strFilter = _
      "(&(objectClass=user)(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=" & strGroupDN & "))"
    objCommand.CommandText = "<LDAP://" & strBaseDN & ">;" & strFilter & ";" & _
      "aDSPath,sAMAccountName;subtree"
   
    Dim objRecordSet : Set objRecordSet = objCommand.Execute
 
    Dim strMembers : strMembers = ""
    Do Until objRecordSet.EOF
      strMembers = strMembers & objRecordSet.Fields("sAMAccountName").Value & _
        "#" & objRecordSet.Fields("aDSPAth") & "#User;"
 
      objDomainGroups(strDomainGroup) = strMembers
 
      objRecordSet.MoveNext
    Loop
 
    Set objRecordSet = Nothing
    Set objCommand = Nothing
    Set objConnection = Nothing
  Next
 
  Set ExpandDomainGroups = objDomainGroups
End Function
 
Function GetObjectDN(strObject)
  ' Return Type: String
  '
  ' Returns a Distinguished Name for an Object from it's NT SAM ID.
  ' This will only function for valid object types within an NT Domain structure.
 
  Const ADS_NAME_INITTYPE_GC = 3
  Const ADS_NAME_TYPE_1779 = 1
  Const ADS_NAME_TYPE_NT4 = 3
 
  On Error Resume Next : Err.Clear
  Dim objNameTranslate : Set objNameTranslate = CreateObject("NameTranslate")
 
  objNameTranslate.Init ADS_NAME_INITTYPE_GC, ""
  objNameTranslate.Set ADS_NAME_TYPE_NT4, strObject
  Dim strObjectDN : strObjectDN = objNameTranslate.Get(ADS_NAME_TYPE_1779)
  If Err.Number <> 0 Then  ' Make the DN Blank for a Failed Search
    strObjectDN = ""
  End If
 
  Set objNameTranslate = Nothing
  On Error Goto 0
 
  GetObjectDN = strObjectDN
End Function
 
Sub WriteResults(objLocalGroups, objDomainGroups)
 
  Dim strLocalGroups
  For Each strLocalGroup in objLocalGroups
    WScript.Echo strLocalGroup & " - " & objLocalGroups(strLocalGroup)(0)
    DisplayMembers strMembers, objDomainGroups
    WScript.Echo
  Next
End Sub
 
Sub DisplayMembers(strMembers, objDomainGroups)
  If strMembers <> "" Then
    Dim arrMembers : arrMembers = Split(strMembers, ";")
 
    Dim i
    For i = 0 To (UBound(arrMembers) - 1)
      Dim strMember : strMember = arrMembers(i)
      Dim arrMember : arrMember = Split(strMember, "#")
 
      If UBound(Split(arrMember(1), "/")) = 3 _
          And InStr(arrMember(1), "NT AUTHORITY") = 0 _
          And arrMember(2) = "Group" Then
 
        Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
        If objDomainGroups.Exists(strDomainGroup) Then
          strMembers = Replace(strMembers, strMember & ";", objDomainGroups(strDomainGroup))
        End If
      End If
    Next
 
    arrMembers = Split(strMembers, ";")
    For i = 0 To (UBound(arrMembers) - 1)
      arrMember = Split(arrMembers(i), "#")
      WScript.Echo arrMember(0)
    Next
  End If
End Sub
 
'
' Main Code
'
 
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")
Dim strDomainDN : strDomainDN = objRootDSE.Get("defaultNamingContext")
Set objRootDSE = Nothing
 
' Alternatives include GC and rootDomainNamingContext
'Dim arrComputers : arrComputers = GetComputersFromAD("LDAP", strDomainDN)
 
'Dim arrBatches : arrBatches = BatchGenerator(arrComputers)
 
'ThreadManager arrBatches
 
Dim objLocalGroups : Set objLocalGroups = CollateResultFiles
Dim objDomainGroups : Set objDomainGroups = GetDomainGroups(objLocalGroups)
Set objDomainGroups = ExpandDomainGroups(objDomainGroups, strDomainDN)
 
WriteResults objLocalGroups, objDomainGroups

Open in new window

ziped them up, get some sleep, work on this error tomorrow :)
C:\admin.vbs(165, 3) Microsoft VBScript runtime error: Variable is
 undefined: 'strLocalGroup'
-rich

I must have forgotten to copy these parts back from my test script. Sorry about this.

Line 165 is currently "Dim strLocalGroups". If just needs the "s" removing from the end. Probably caused by keyboard mashing Alt, F, S to save the file ;)

Chris
C:\admin.vbs(195, 22) Microsoft VBScript runtime error: Variable i
s undefined: 'strGroupDN'
Just in case I messed it up I've attached the code I'm using.,.
-rich
Option Explicit
 
' Script Constants
 
' Maximum number of computers to add into each thread
Const MAX_PER_THREAD = 10
' Maximum number of threads to execute
Const MAX_THREADS = 10
 
' Functions
 
Function GetComputersFromAD(strPort, strBaseDN)
  ' Returns an array containing all computers in AD. Performed here so we do not have to maintain 
  ' a connection to AD for any longer than necessary. Valid Ports are LDAP or GC.
 
  Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
  objConnection.Provider = "ADsDSOObject"
  objConnection.Open "Active Directory Provider"
 
  Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
  Set objCommand.ActiveConnection = objConnection
  objCommand.Properties("Page Size") = 1000
 
  Dim strFilter : strFilter = "(&(objectClass=computer)(!frscomputerreferencebl=*))"
  objCommand.CommandText = "<" & strPort & "://" & strBaseDN & ">;" & strFilter & ";" & _
    "name;subtree"
  
  Dim objRecordSet : Set objRecordSet = objCommand.Execute
 
  Dim arrComputers()
  Dim i : i = 0
 
  Do Until objRecordSet.EOF
    ReDim Preserve arrComputers(i)
    arrComputers(i) = objRecordSet.Fields("name").Value
    i = i + 1
    objRecordSet.MoveNext
  Loop
 
  Set objRecordSet = Nothing
  Set objCommand = Nothing
  Set objConnection = Nothing
 
  GetComputersFromAD = arrComputers
End Function
 
Function BatchGenerator(arrComputers)
  Dim intTotalComputers : intTotalComputers = UBound(arrComputers) + 1
  Dim intPerThread : intPerThread = MAX_PER_THREAD
 
  ' Reduce the maximum number per thread if it will leave threads in the pool doing nothing
  If (intTotalComputers / MAX_THREADS) < MAX_PER_THREAD Then
    intPerThread = Round(intTotalComputers / MAX_THREADS)
  End If
 
  Dim intCurrentBatchCount : intCurrentBatchCount = 0
  Dim intBatchNo : intBatchNo = 0
  Dim arrBatches() : ReDim arrBatches(intBatchNo)
  Dim strComputer
  For Each strComputer in arrComputers
    If intCurrentBatchCount = intPerThread Then
      intCurrentBatchCount = 0
      intBatchNo = intBatchNo + 1
      ReDim Preserve arrBatches(intBatchNo)
    End If
 
    intCurrentBatchCount = intCurrentBatchCount + 1
    arrBatches(intBatchNo) = arrBatches(intBatchNo) & strComputer & ";"
  Next
  BatchGenerator = arrBatches
End Function
 
Sub ThreadManager(arrBatches)
  Dim objShell : Set objShell = CreateObject("WScript.Shell")
 
  Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
 
  Dim objFolder, objFile
  Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
  For Each objFile in objFolder.Files
    If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
      objFile.Delete
    End If
  Next
 
  Dim objThreadMonitor : Set objThreadMonitor = CreateObject("Scripting.Dictionary")
 
  Dim i : i = 0
  Do Until i = UBound(arrBatches)
 
    ' Start Threads if Threads can be started
    If objThreadMonitor.Count < MAX_THREADS Then
      ' WScript.Echo "Starting " & i
      objThreadMonitor.Add i, ""
      objShell.Run "cscript.exe ChildScript.vbs " & arrBatches(i) & " " & i, 0, False
      i = i + 1
    Else
      ' WScript.Echo "Sleeping for 10 seconds"
      WScript.Sleep 10000
    End If
 
    ' Look for Finished Threads
    Dim j
    For Each j in objThreadMonitor
      If objFileSystem.FileExists(j & ".results") Then
        ' WScript.Echo "Stopping " & j
        objThreadMonitor.Remove j
      End If
    Next
  Loop
 
  ' Wait for completion of remaining threads
  Do Until objThreadMonitor.Count = 0
    ' WScript.Echo "Batches complete. Waiting for Thread Shutdown"
    WScript.Sleep 10000
 
    For Each j in objThreadMonitor
      If objFileSystem.FileExists(j & ".results") Then
        ' WScript.Echo "Stopping " & j
        objThreadMonitor.Remove j
      End If
    Next
  Loop
 
  Set objFileSystem = Nothing
  Set objThreadMonitor = Nothing
  Set objShell = Nothing
End Sub
 
Function CollateResultFiles
  Dim objFileSystem : Set objFileSystem = CreateObject("Scripting.FileSystemObject")
  Dim objFolder : Set objFolder = objFileSystem.GetFolder(Replace(WScript.ScriptFullName, WScript.ScriptName, ""))
 
  Dim objLocalGroups : Set objLocalGroups = CreateObject("Scripting.Dictionary")
  objLocalGroups.CompareMode = VbTextCompare
 
  Dim objFile
  For Each objFile in objFolder.Files
    If objFileSystem.GetExtensionName(objFile.Path) = "results" Then
      Dim objStream : Set objStream = objFile.OpenAsTextStream(1, 0)
 
      Do Until objStream.AtEndOfStream
        Dim arrLine : arrLine = Split(objStream.ReadLine, "|")
        Dim strComputer : strComputer = arrLine(0)
        Dim strOS : strOS = arrLine(1)
        Dim strGroup : strGroup = arrLine(2)
        Dim strMembers : strMembers = arrLine(3)
 
        objLocalGroups.Add strComputer & "/" & strGroup, Array(strOS, strMembers)
      Loop
 
      Set objStream = Nothing
    End If
  Next
 
  Set CollateResultFiles = objLocalGroups
End Function
 
Function GetDomainGroups(objLocalGroups)
  ' Create a list of domain group names from the data returned
 
  Dim objDomainGroups : Set objDomainGroups = CreateObject("Scripting.Dictionary")
 
  Dim strLocalGroup
  For Each strLocalGroup in objLocalGroups
    Dim strMembers : strMembers = objLocalGroups(strLocalGroup)(1)
 
    If strMembers <> "" Then
      Dim arrMembers : arrMembers = Split(strMembers, ";")
 
      Dim i
      For i = 0 To (UBound(arrMembers) - 1)
        Dim arrMember : arrMember = Split(arrMembers(i), "#")
 
        If UBound(Split(arrMember(1), "/")) = 3 _
            And InStr(arrMember(1), "NT AUTHORITY") = 0 _
            And arrMember(2) = "Group" Then
 
          Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
          If Not objDomainGroups.Exists(strDomainGroup) Then
            objDomainGroups.Add strDomainGroup, ""
          End If
        End If
      Next
    End If
  Next
  Set GetDomainGroups = objDomainGroups
End Function
 
Function ExpandDomainGroups(objDomainGroups, strBaseDN)
  ' This method requires Windows 2003 SP1 minimum
 
  Dim strDomainGroup
  For Each strDomainGroup in objDomainGroups
    Dim objGroupDN : strGroupDN = GetObjectDN(strDomainGroup)
 
    Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
    objConnection.Provider = "ADsDSOObject"
    objConnection.Open "Active Directory Provider"
 
    Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
    Set objCommand.ActiveConnection = objConnection
    objCommand.Properties("Page Size") = 1000
 
    Dim strFilter : strFilter = _
      "(&(objectClass=user)(objectCategory=person)(memberOf:1.2.840.113556.1.4.1941:=" & strGroupDN & "))"
    objCommand.CommandText = "<LDAP://" & strBaseDN & ">;" & strFilter & ";" & _
      "aDSPath,sAMAccountName;subtree"
   
    Dim objRecordSet : Set objRecordSet = objCommand.Execute
 
    Dim strMembers : strMembers = ""
    Do Until objRecordSet.EOF
      strMembers = strMembers & objRecordSet.Fields("sAMAccountName").Value & _
        "#" & objRecordSet.Fields("aDSPAth") & "#User;"
 
      objDomainGroups(strDomainGroup) = strMembers
 
      objRecordSet.MoveNext
    Loop
 
    Set objRecordSet = Nothing
    Set objCommand = Nothing
    Set objConnection = Nothing
  Next
 
  Set ExpandDomainGroups = objDomainGroups
End Function
 
Function GetObjectDN(strObject)
  ' Return Type: String
  '
  ' Returns a Distinguished Name for an Object from it's NT SAM ID.
  ' This will only function for valid object types within an NT Domain structure.
 
  Const ADS_NAME_INITTYPE_GC = 3
  Const ADS_NAME_TYPE_1779 = 1
  Const ADS_NAME_TYPE_NT4 = 3
 
  On Error Resume Next : Err.Clear
  Dim objNameTranslate : Set objNameTranslate = CreateObject("NameTranslate")
 
  objNameTranslate.Init ADS_NAME_INITTYPE_GC, ""
  objNameTranslate.Set ADS_NAME_TYPE_NT4, strObject
  Dim strObjectDN : strObjectDN = objNameTranslate.Get(ADS_NAME_TYPE_1779)
  If Err.Number <> 0 Then  ' Make the DN Blank for a Failed Search
    strObjectDN = ""
  End If
 
  Set objNameTranslate = Nothing
  On Error Goto 0
 
  GetObjectDN = strObjectDN
End Function
 
Sub WriteResults(objLocalGroups, objDomainGroups)
 
  Dim strLocalGroups
  For Each strLocalGroup in objLocalGroups
    WScript.Echo strLocalGroup & " - " & objLocalGroups(strLocalGroup)(0)
    DisplayMembers strMembers, objDomainGroups
    WScript.Echo
  Next
End Sub
 
Sub DisplayMembers(strMembers, objDomainGroups)
  If strMembers <> "" Then
    Dim arrMembers : arrMembers = Split(strMembers, ";")
 
    Dim i
    For i = 0 To (UBound(arrMembers) - 1)
      Dim strMember : strMember = arrMembers(i)
      Dim arrMember : arrMember = Split(strMember, "#")
 
      If UBound(Split(arrMember(1), "/")) = 3 _
          And InStr(arrMember(1), "NT AUTHORITY") = 0 _
          And arrMember(2) = "Group" Then
 
        Dim strDomainGroup : strDomainGroup = Split(arrMember(1), "/")(2) & "\" & arrMember(0)
        If objDomainGroups.Exists(strDomainGroup) Then
          strMembers = Replace(strMembers, strMember & ";", objDomainGroups(strDomainGroup))
        End If
      End If
    Next
 
    arrMembers = Split(strMembers, ";")
    For i = 0 To (UBound(arrMembers) - 1)
      arrMember = Split(arrMembers(i), "#")
      WScript.Echo arrMember(0)
    Next
  End If
End Sub
 
'
' Main Code
'
 
Dim objRootDSE : Set objRootDSE = GetObject("LDAP://RootDSE")
Dim strDomainDN : strDomainDN = objRootDSE.Get("defaultNamingContext")
Set objRootDSE = Nothing
 
' Alternatives include GC and rootDomainNamingContext
'Dim arrComputers : arrComputers = GetComputersFromAD("LDAP", strDomainDN)
 
'Dim arrBatches : arrBatches = BatchGenerator(arrComputers)
 
'ThreadManager arrBatches
 
Dim objLocalGroups : Set objLocalGroups = CollateResultFiles
Dim objDomainGroups : Set objDomainGroups = GetDomainGroups(objLocalGroups)
Set objDomainGroups = ExpandDomainGroups(objDomainGroups, strDomainDN)
 
WriteResults objLocalGroups, objDomainGroups

Open in new window


Definitely failed to copy back the code from my test script.

> Dim objGroupDN : strGroupDN = GetObjectDN(strDomainGroup)

Should be:

Dim strGroupDN : strGroupDN = GetObjectDN(strDomainGroup)

Chris
C:\admin.vbs(259, 3) Microsoft VBScript runtime error: Variable is undefined: 'strLocalGroup'
-rich

Same again, plural instead of singular. On that line:

> Dim strLocalGroups

Should be:

Dim strLocalGroup

Sorry for the slow responses, I try not to post too much / check mail over the weekends.

Chris
Np, didn't expect anything til monday or after ;)
C:\-admin.vbs(261, 5) Microsoft VBScript runtime error: Variable is undefined: 'strMembers'
added Dim strMembers, seems to be working... The OS seems to repeat with each group...
-rich

Good morning :)

Recoding the output into XML. I'll post an amendment with a proper round of bug-checking first.

Chris
ASKER CERTIFIED SOLUTION
Avatar of Chris Dent
Chris Dent
Flag of United Kingdom of Great Britain and Northern Ireland image

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
SOLUTION
Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Jesus... you've out done yourself... running it now...
One thing I noticed, and I'm sure it's the nature of the beast and probably can't be helped, but killing the main script still allows the spawned scripts to continue. I know they are their own process, and I'm pretty sure there isn't much that can be done about it, again you all have exceeded all expectations!
-rich

Yeah, they'll continue until they complete. The only way to kill it off completely is if we wrote another script to terminate any processes running CScript.exe (a bit tricky because it'll have to identify its own process).

Chris
Looks like this is working well! I'm going to open another question in a few, related to the same type of multi-threaded scanning and shares.
-rich
Again, above and beyond!

Before leaving this one.

I rewrote the thread management part (again). Splitting it into it's own moderately generic script. It accepts the name of the child script as an argument (along with a few optional arguments).

I figure we can just shift the part that deals with concatenating the results into the script executed by the FinalCommand parameter. Thread management relies on the return from Win32_Process now.

Hopefully that means all we have to do for the other tasks is concoct the child script, the concatenate the return in the final script.

Chris
Option Explicit
 
' Searches the domain for computers, executes a child script under a number of seperate processes for each
' batch of computers. Passes computer list to child as a semi-colon delimited list.
' 
' Uses Win32_Process to monitor threads.
'
' Author: Chris Dent
' Last Modified: 17/02/2009
 
Sub UsageText
  Dim strUsage : strUsage = "Usage:" & vbCrLf & vbCrLf
  strUsage = strUsage & WScript.ScriptName & " /ChildScript:<ScriptName> [/FinalScript:<ScriptName>]" & vbCrLf
  strUsage = strUsage & "        [/NumComputers:<Integer>] [/NumThreads:<Integer>]" & vbCrLf
  strUsage = strUsage & "        [/Server:<Name>] [/BaseDN:<Distinguished Name>] [/LDAPFilter:<Filter String>]" & vbCrLf
  strUsage = strUsage & "        [/GlobalCatalog] [/OneLevel]" & vbCrLf & vbCrLf
  strUsage = strUsage & "Required arguments:" & vbCrLf & vbCrlf
  strUsage = strUsage & "    ChildScript       Script to execute as batch job" & vbCrLf & vbCrLf
  strUsage = strUsage & "Optional arguments: " & vbCrlf & vbCrLf
  strUsage = strUsage & "    FinalCommand      Command to execute after all processing is complete." & vbCrLf
  strUsage = strUsage & "    NumComputers      Maximum number of computer objects to assign to each batch. (Default 10)" & vbCrLf
  strUsage = strUsage & "    NumThreads        Maximum number of threads to execute simultaneously. (Default 10)" & vbCrLf
  strUsage = strUsage & "    Server            LDAP server for query. (Default logon server)" & vbCrLf
  strUsage = strUsage & "    BaseDN            Base distinguished name or OU used for search. (Default current domain)" & vbCrLf
  strUsage = strUsage & "    LDAPFilter        LDAP Filter used when performing search. (Default objectClass=computer)" & vbCrLf
  strUsage = strUsage & "    GlobalCatalog     Execute the search against a Global Catalog." & vbCrLf
  strUsage = strUsage & "    OneLevel          Execute the search for this level only (Default subtree)." & vbCrLf
 
  WScript.Echo strUsage
  WScript.Quit
End Sub
 
Function GetArgs
  Dim objArgs : Set objArgs = CreateObject("Scripting.Dictionary")
 
  Dim strChildScript : strChildScript = WScript.Arguments.Named("ChildScript")
  If strChildScript = "" Then
    UsageText
  End If
  Dim objFSO : Set objFSO = CreateObject("Scripting.FileSystemObject")  
  If Not objFSO.FileExists(strChildScript) Then
    WScript.Echo "ERROR: Child script not found" & vbCrLf
    UsageText
  End If
  objArgs.Add "ChildScript", strChildScript
  Set objFSO = Nothing
 
  If WScript.Arguments.Named("FinalCommand") <> "" Then
    objArgs.Add "FinalCommand", WScript.Arguments.Named("FinalCommand")
  End If
 
  Dim strNumComputers : strNumComputers = WScript.Arguments.Named("NumComputers")
  If strNumComputers <> "" Then
    If IsNumeric(strNumComputers) Then
      objArgs.Add "NumComputers", CInt(strNumComputers)
    Else
      WScript.Echo "ERROR: NumComputers must be an integer value" & vbCrLf
    End If
  Else
    objArgs.Add "NumComputers", 10
  End If
 
  Dim strNumThreads : strNumThreads = WScript.Arguments.Named("NumThreads")
  If strNumThreads <> "" Then
    If IsNumeric(strNumThreads) Then
      objArgs.Add "NumThreads", CInt(strNumThreads)
    Else
      WScript.Echo "ERROR: NumThreads must be an integer value" & vbCrLf
    End If
  Else
    objArgs.Add "NumThreads", 10
  End If
 
  objArgs.Add "Server", ""  
  If WScript.Arguments.Named("Server") <> "" Then
    objArgs("Server") = WScript.Arguments.Named("Server") & "/"
  End If
 
  If WScript.Arguments.Named("BaseDN") <> "" Then
    objArgs.Add "BaseDN", WScript.Arguments.Named("BaseDN")
  End If
  If Not objArgs.Exists("BaseDN") Then
    Dim objRootDSE : Set objRootDSE = GetObject("LDAP://" & objArgs("Server") & "RootDSE")
    objArgs.Add "BaseDN", objRootDSE.Get("defaultNamingContext")
    Set objRootDSE = Nothing
  End If
 
  objArgs.Add "LDAPFilter", "(objectClass=computer)"
  If WScript.Arguments.Named("LDAPFilter") <> "" Then
    objArgs("LDAPFilter") = WScript.Arguments.Named("LDAPFilter")
  End If
 
  objArgs.Add "Port", "LDAP"
  objArgs.Add "Scope", "subtree"
  
  Dim strArg
  For Each strArg in WScript.Arguments
    If LCase(strArg) = "/globalcatalog" Then
      objArgs("Port") = "GC"
    ElseIf LCase(strArg) = "/onelevel" Then
      objArgs("Scope") = "onelevel"
    End If
  Next
 
  Set GetArgs = objArgs
End Function
 
Function GetComputersFromAD(objArgs)
  ' Returns an array containing all computers in AD. Performed here so we do not have to maintain 
  ' a connection to AD for any longer than necessary. Valid Ports are LDAP or GC.
 
  Dim objConnection : Set objConnection = CreateObject("ADODB.Connection")
  objConnection.Provider = "ADsDSOObject"
  objConnection.Open "Active Directory Provider"
 
  Dim objCommand : Set objCommand = CreateObject("ADODB.Command")
  Set objCommand.ActiveConnection = objConnection
  objCommand.Properties("Page Size") = 1000
 
  objCommand.CommandText = "<" & objArgs("Port") & "://" & objArgs("Server") & objArgs("BaseDN") & _
    ">;" & objArgs("LDAPFilter") & ";" & "name;" & objArgs("Scope")
  
  Dim objRecordSet : Set objRecordSet = objCommand.Execute
 
  Dim arrComputers()
  Dim i : i = 0
 
  Do Until objRecordSet.EOF
    ReDim Preserve arrComputers(i)
    arrComputers(i) = objRecordSet.Fields("name").Value
    i = i + 1
    objRecordSet.MoveNext
  Loop
 
  Set objRecordSet = Nothing
  Set objCommand = Nothing
  Set objConnection = Nothing
 
  GetComputersFromAD = arrComputers
End Function
 
Function BatchGenerator(arrComputers, objArgs)
  Dim intTotalComputers : intTotalComputers = UBound(arrComputers) + 1
  Dim intPerThread : intPerThread = objArgs("NumComputers")
 
  ' Reduce the maximum number per thread if it will leave threads in the pool doing nothing
  If (intTotalComputers / objArgs("NumThreads")) < objArgs("NumComputers") Then
    intPerThread = Round(intTotalComputers / objArgs("NumThreads"))
  End If
 
  Dim intCurrentBatchCount : intCurrentBatchCount = 0
  Dim intBatchNo : intBatchNo = 0
  Dim arrBatches() : ReDim arrBatches(intBatchNo)
  Dim strComputer
  For Each strComputer in arrComputers
    If intCurrentBatchCount = intPerThread Then
      intCurrentBatchCount = 0
      intBatchNo = intBatchNo + 1
      ReDim Preserve arrBatches(intBatchNo)
    End If
 
    intCurrentBatchCount = intCurrentBatchCount + 1
    arrBatches(intBatchNo) = arrBatches(intBatchNo) & strComputer & ";"
  Next
  For intBatchNo = 0 to UBound(arrBatches)
    arrBatches(intBatchNo) = Left(arrBatches(intBatchNo), Len(arrBatches(intBatchNo)) - 1)
  Next
 
  BatchGenerator = arrBatches
End Function
 
Sub ThreadManager(arrBatches, objArgs)
  ' Starts multiple script processes and waits for completion
 
  Dim objShell : Set objShell = CreateObject("WScript.Shell")
 
  ' Execute thread for each batch
  Dim i : i = 0
  Do Until i = (UBound(arrBatches) + 1)
    If GetProcessCount(objArgs("ChildScript")) < objArgs("NumThreads") Then
      WScript.Echo "Starting Batch " & (i + 1) & " of " & (UBound(arrBatches) + 1) 
      objShell.Run "cscript.exe " & objArgs("ChildScript") & " " & arrBatches(i), 0, False
      i = i + 1
    Else
      WScript.Sleep 10000
    End If
  Loop
 
  ' Wait for completion of remaining threads
  Do Until GetProcessCount(objArgs("ChildScript")) = 0
    WScript.Sleep 10000
  Loop
 
  Set objShell = Nothing
End Sub
 
Function GetProcessCount(strChildScript)
  ' Returns the number of cscript or wscript processes executing ChildScript.
 
  Dim objWMI : Set objWMI = GetObject("winmgmts:\\.\root\cimv2")
  Dim colItems : Set colItems = _
    objWMI.ExecQuery("SELECT CommandLine, Name, ProcessId " & _
    "FROM Win32_Process WHERE Name LIKE '_script.exe' AND CommandLine LIKE '% " & _
    strChildScript & " %'")
 
  GetProcessCount = colItems.Count
 
  Set colItems = Nothing
  Set objWMI = Nothing
End Function
 
Sub ExecuteFinal(objArgs)
 
  If objArgs.Exists("FinalCommand") Then
    Dim objShell : Set objShell = CreateObject("WScript.Shell")
    objShell.Run objArgs("FinalCommand"), 0, True
    Set objShell = Nothing
  End If
End Sub
 
'
' Main code
'
 
Dim objArgs : Set objArgs = GetArgs
Dim arrComputers : arrComputers = GetComputersFromAD(objArgs)
Dim arrBatches : arrBatches = BatchGenerator(arrComputers, objArgs)
ThreadManager arrBatches, objArgs
ExecuteFinal(objArgs)

Open in new window