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

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