Powershell and multi-threading -

Soldato
Joined
8 Mar 2005
Posts
3,686
Location
London, UK
So, know nothing about powershell at all so am after some guidance about going about multithreading a powershell job/function. I'm using the vanilla Get-LastLogon function (http://gallery.technet.microsoft.com/scriptcenter/Get-LastLogon-Determining-283f98ae#content) which pulls last known loggedon user from a windows system.

I have multiple systems to check so I can wrap that function up as follows: "Get-LastLogon (get-content .\serverlist.txt) | export.csv <somepath>\<somefile.csv"

The above will also pipe the output to a csv file. This works, but is exceptionally slow as it processes each system sequentially. I have limited knowledge of powershell (and everything else :) ) but I understand I need to somehow wrap the whole lot up using the Start-Job cmdlet but I have no idea how I would go about that.

Although there many posts about use of the Start-Job cmdlet, I'm struggling to tie it all together in addition to exporting the output to csv.

Cheers, Paul.

EDIT - http://www.get-blog.com/?p=22 - this explains multi-threading perfectly, although I'll need to tailor the Get-LastLogon function to work using this method. Question has evolved to how do you export the output from the multil-threaded instances of Get-LastLogon in to a single csv file. Hmmmm
 
Last edited:
Taking a guess, but where he's doing:
Code:
Get-Job | Receive-Job | Out-GridView
could you not do:
Code:
Get-Job | Receive-Job | Export-CSV

From the looks of things, the get-job|receive-job just globs it all together, although there may be additional headings included. You could likely easily sort and cut those out in Excel or similar?
 
Edit, ignore

Can't you use the lastlogontimestamp property (2003 and above) ? I have written the below using quest ad module and it is fairly quick over thousands of users. I filter certain OUs that I am responsible for but you can take that part out.

Code:
#Imports Quest Active Directory Snapin (must be installed)
Add-PSSnapin Quest.ActiveRoles.ADManagement

$now=get-date

#Threshold before an account is considered inactive
$daysSinceLastLogon=90

#create empty array
$myCol = @()

#defines the OUs were going to search against
$searchbase = 
		"OU=example1,DC=domain,DC=com",
		"OU=example2,DC=domain,DC=com"
		
#start of the loop to search the OUs above

foreach ($ou in $searchbase){
$myusers = Get-QADUser -includedproperties co -enabled -SearchRoot $ou -sizeLimit 0 | where {
  $_.lastlogontimestamp -and
    (($now-$_.lastlogontimestamp).days -gt $daysSinceLastLogon)
} | Select-Object Name,sAMAccountName,co,LastLogonTimeStamp,description,ADsPath

#populates array with results
$myCol += $myusers
}

#pipes results from array to csv
$myCol | export-csv c:\InactiveUsers.csv
 
Last edited:
Uhtred, you should be able to make your script a little more streamlined like this:

Code:
#Imports Quest Active Directory Snapin (must be installed)
Add-PSSnapin Quest.ActiveRoles.ADManagement

#Number of days to go back
$threshold = (Get-Date).AddDays(-90)

#create empty array
$results = @()

#defines the OUs were going to search against
$searchbase = 
		"OU=example1,DC=domain,DC=com",
		"OU=example2,DC=domain,DC=com"
		
#start of the loop to search the OUs above

foreach ($ou in $searchbase)
{
	$results += Get-QADUser -includedproperties co -enabled -SearchRoot $ou -sizeLimit 0 | ? {$_.lastlogontimestamp -lt $threshold} | Select-Object Name,sAMAccountName,co,LastLogonTimeStamp,description,ADsPath
}

#pipes results from array to csv
$results | export-csv c:\InactiveUsers.csv

I'm not certain if that is what the OP is asking for though, seems like he's asking about logins to individual servers/systems as opposed to just users that haven't logged in recently.
 
Ah you're right of course, sorry it serves me right trying to watch the football at the same time :p

Thanks for the tip though, I might be able to apply that to another script I use. I'll have to come in here more often ;)
 
I write a lot of PowerShell scripts where I work now :)

By no means an expert or anything but I can generally get what I want to work, or have an idea where to start on it.

I must say I really like the Quest AD plugins, used them a few times now, really good for working with AD users and computer objects!

Pretty sure you can use the -InactiveFor switch with Get-QADUser to return users that haven't logged in for a while as well.
 
Taking a guess, but where he's doing:
Code:
Get-Job | Receive-Job | Out-GridView
could you not do:
Code:
Get-Job | Receive-Job | Export-CSV

Alas, that last pipe does actually export either to the screen or to csv, seems to be a redundant command. I suspect the individual instances that Get-Job launches does not pass the result,output back to the parent thread, which initiates the actual instances.

Yes to clarify, In this exercise I'm after the last known USER logon, whether it be a local account or AD account on the actual system. The GetLastLogon script works perfectly for this albeit it processes sequentially (which is what I wanted to speed up), although I need to add additional FilterSID as its picking up additional system/functional accounts I want to exclude.

Cheers, Paul.

EDIT3 - Fixed, just need to define PARAM($computer) (head in hands) - So just need to understand how to add additional FilterSID to the Get-LastLogon cmdlet.


Get-LastLogon with modified lines (Brian C. Wilhite is the original author)
Code:
Param($computer)
Function Get-LastLogon2 
{ 
<# 
 
.SYNOPSIS 
  This function will list the last user logged on or logged in. 
 
.DESCRIPTION 
  This function will list the last user logged on or logged in.  It will detect if the user is currently logged on 
  via WMI or the Registry, depending on what version of Windows is running on the target.  There is some "guess" work 
  to determine what Domain the user truly belongs to if run against Vista NON SP1 and below, since the function 
  is using the profile name initially to detect the user name.  It then compares the profile name and the Security 
  Entries (ACE-SDDL) to see if they are equal to determine Domain and if the profile is loaded via the Registry. 
 
.PARAMETER ComputerName 
  A single Computer or an array of computer names.  The default is localhost ($env:COMPUTERNAME). 
 
.PARAMETER FilterSID 
  Filters a single SID from the results.  For use if there is a service account commonly used. 
   
.PARAMETER WQLFilter 
  Default WQLFilter defined for the Win32_UserProfile query, it is best to leave this alone, unless you know what 
  you are doing. 
  Default Value = "NOT SID = 'S-1-5-18' AND NOT SID = 'S-1-5-19' AND NOT SID = 'S-1-5-20'" 
   
.EXAMPLE 
  $Servers = Get-Content "C:\ServerList.txt" 
  Get-LastLogon -ComputerName $Servers 
 
  This example will return the last logon information from all the servers in the C:\ServerList.txt file. 
 
  Computer          : SVR01 
  User              : WILHITE\BRIAN 
  SID               : S-1-5-21-012345678-0123456789-012345678-012345 
  Time              : 9/20/2012 1:07:58 PM 
  CurrentlyLoggedOn : False 
 
  Computer          : SVR02 
  User              : WILIHTE\BRIAN 
  SID               : S-1-5-21-012345678-0123456789-012345678-012345 
  Time              : 9/20/2012 12:46:48 PM 
  CurrentlyLoggedOn : True 
   
.EXAMPLE 
  Get-LastLogon -ComputerName svr01, svr02 -FilterSID S-1-5-21-012345678-0123456789-012345678-012345 
 
  This example will return the last logon information from all the servers in the C:\ServerList.txt file. 
 
  Computer          : SVR01 
  User              : WILHITE\ADMIN 
  SID               : S-1-5-21-012345678-0123456789-012345678-543210 
  Time              : 9/20/2012 1:07:58 PM 
  CurrentlyLoggedOn : False 
 
  Computer          : SVR02 
  User              : WILIHTE\ADMIN 
  SID               : S-1-5-21-012345678-0123456789-012345678-543210 
  Time              : 9/20/2012 12:46:48 PM 
  CurrentlyLoggedOn : True 
 
.LINK 
  http://msdn.microsoft.com/en-us/library/windows/desktop/ee886409(v=vs.85).aspx 
  http://msdn.microsoft.com/en-us/library/system.security.principal.securityidentifier.aspx 
 
.NOTES 
  Author:   Brian C. Wilhite 
  Email:   [email protected] 
  Date:    "09/20/2012" 
  Updates: Added FilterSID Parameter 
           Cleaned Up Code, defined fewer variables when creating PSObjects 
  ToDo:    Clean up the UserSID Translation, to continue even if the SID is local 
#> 
 
[CmdletBinding()] 
param( 
  [Parameter(Position=0,ValueFromPipeline=$true)] 
  [Alias("CN","Computer")] 
  [String[]]$ComputerName="$env:COMPUTERNAME", 
  [String]$FilterSID="S-1-5-21-1229272821-1123561945-682003330-1693969",
  [String]$FilterSID2="S-1-5-21-1229272821-1123561945-682003330-1824435",  
  [String]$WQLFilter="NOT SID = 'S-1-5-18' AND NOT SID = 'S-1-5-19' AND NOT SID = 'S-1-5-20'" 
  ) 
 
Begin 
  { 
    #Adjusting ErrorActionPreference to stop on all errors 
    $TempErrAct = $ErrorActionPreference 
    $ErrorActionPreference = "Stop" 
    #Exclude Local System, Local Service & Network Service 
  }#End Begin Script Block 
 
Process 
  { 
    Foreach ($Computer in $ComputerName) 
      { 
        $Computer = $Computer.ToUpper().Trim() 
        Try 
          { 
            #Querying Windows version to determine how to proceed. 
            $Win32OS = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $Computer 
            $Build = $Win32OS.BuildNumber 
             
            #Win32_UserProfile exist on Windows Vista and above 
            If ($Build -ge 6001) 
              { 
                If ($FilterSID -Or $FilterSID2) 
                  { 
                    #$WQLFilter = $WQLFilter + " AND NOT SID = `'$FilterSID`'"# AND NOT SID = `'$FilterSID2`'"
                    $WQLFilter = $WQLFilter + " AND NOT SID = `'$FilterSID`'" + " AND NOT SID = `'$FilterSID2`'" 
                  }#End If ($FilterSID -Or $FilterSID2) 
                $Win32User = Get-WmiObject -Class Win32_UserProfile -Filter $WQLFilter -ComputerName $Computer 
                $LastUser = $Win32User | Sort-Object -Property LastUseTime -Descending | Select-Object -First 1 
                $Loaded = $LastUser.Loaded 
                $Script:Time = ([WMI]'').ConvertToDateTime($LastUser.LastUseTime) 
                 
                #Convert SID to Account for friendly display 
                $Script:UserSID = New-Object System.Security.Principal.SecurityIdentifier($LastUser.SID) 
                $User = $Script:UserSID.Translate([System.Security.Principal.NTAccount]) 
              }#End If ($Build -ge 6001) 
               
            If ($Build -le 6000) 
              { 
                If ($Build -eq 2195) 
                  { 
                    $SysDrv = $Win32OS.SystemDirectory.ToCharArray()[0] + ":" 
                  }#End If ($Build -eq 2195) 
                Else 
                  { 
                    $SysDrv = $Win32OS.SystemDrive 
                  }#End Else 
                $SysDrv = $SysDrv.Replace(":","$") 
                $Script:ProfLoc = "\\$Computer\$SysDrv\Documents and Settings" 
                $Profiles = Get-ChildItem -Path $Script:ProfLoc 
                $Script:NTUserDatLog = $Profiles | ForEach-Object -Process {$_.GetFiles("ntuser.dat.LOG")} 
                 
                #Function to grab last profile data, used for allowing -FilterSID to function properly. 
                function GetLastProfData ($InstanceNumber) 
                  { 
                    $Script:LastProf = ($Script:NTUserDatLog | Sort-Object -Property LastWriteTime -Descending)[$InstanceNumber]               
                    $Script:UserName = $Script:LastProf.DirectoryName.Replace("$Script:ProfLoc","").Trim("\").ToUpper() 
                    $Script:Time = $Script:LastProf.LastAccessTime 
                     
                    #Getting the SID of the user from the file ACE to compare 
                    $Script:Sddl = $Script:LastProf.GetAccessControl().Sddl 
                    $Script:Sddl = $Script:Sddl.split("(") | Select-String -Pattern "[0-9]\)$" | Select-Object -First 1 
                    #Formatting SID, assuming the 6th entry will be the users SID. 
                    $Script:Sddl = $Script:Sddl.ToString().Split(";")[5].Trim(")") 
                     
                    #Convert Account to SID to detect if profile is loaded via the remote registry 
                    $Script:TranSID = New-Object System.Security.Principal.NTAccount($Script:UserName) 
                    $Script:UserSID = $Script:TranSID.Translate([System.Security.Principal.SecurityIdentifier]) 
                  }#End function GetLastProfData 
                GetLastProfData -InstanceNumber 0 
                 
                #If the FilterSID equals the UserSID, rerun GetLastProfData and select the next instance 
                If ($Script:UserSID -eq $FilterSID -Or $Script:UserSID -eq $FilterSID2) 
                  { 
                    GetLastProfData -InstanceNumber 1 
                  }#End If ($Script:UserSID -eq $FilterSID) -Or ($Script:UserSID -eq $FilterSID2) 
                 
                #If the detected SID via Sddl matches the UserSID, then connect to the registry to detect currently loggedon. 
                If ($Script:Sddl -eq $Script:UserSID) 
                  { 
                    $Reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey([Microsoft.Win32.RegistryHive]"Users",$Computer) 
                    $Loaded = $Reg.GetSubKeyNames() -contains $Script:UserSID.Value 
                    #Convert SID to Account for friendly display 
                    $Script:UserSID = New-Object System.Security.Principal.SecurityIdentifier($Script:UserSID) 
                    $User = $Script:UserSID.Translate([System.Security.Principal.NTAccount]) 
                  }#End If ($Script:Sddl -eq $Script:UserSID) 
                Else 
                  { 
                    $User = $Script:UserName 
                    $Loaded = "Unknown" 
                  }#End Else 
 
              }#End If ($Build -le 6000) 
             
            #Creating Custom PSObject For Output 
            New-Object -TypeName PSObject -Property @{ 
              Computer=$Computer 
              User=$User 
              SID=$Script:UserSID 
              Time=$Script:Time 
              CurrentlyLoggedOn=$Loaded 
              } | Select-Object Computer, User, SID, Time, CurrentlyLoggedOn 
               
          }#End Try 
           
        Catch 
          { 
            If ($_.Exception.Message -Like "*Some or all identity references could not be translated*") 
              { 
                Write-Warning "Unable to Translate $Script:UserSID, try filtering the SID `nby using the -FilterSID parameter."   
                Write-Warning "It may be that $Script:UserSID is local to $Computer, Unable to translate remote SID" 
              } 
            Else 
              { 
                Write-Warning $_ 
              } 
          }#End Catch 
           
      }#End Foreach ($Computer in $ComputerName) 
       
  }#End Process 
   
End 
  { 
    #Resetting ErrorActionPref 
    $ErrorActionPreference = $TempErrAct 
  }#End End 
 
}# End Function Get-LastLogon
#Param($ComputerName = "LocalHost")
Get-Lastlogon2 -ComputerName $Computer
Originally the code allowed you to pass a single variable -FilterSID, however I need to pass multiple or simply just define them within the params section which I've done above, however the if loops with included -Or do not appear to work as expected and the function will continue just to exclude a single FilterSID variable only.

EDIT4 - After some further checks it does work as expected for windows 6001+ builds(Vista/Windows7/Windows2008). It does not work with WinXP so looks like the logic isn't right in the section If ($build -le 6000)
 
Last edited:
Back
Top Bottom