1

i've created a WPF PowerShell application which functions OK. But sometimes when entering another user ID it gets on not responding for a while before showing some output. I've searched the internet and I can resolve this by adding runspaces. But I need some help inserting this in my script.. Or is there another way to make it more responsive?

Below my script:

## Add .xaml file, remove errors and set variables to use them in this script

Add-Type -AssemblyName PresentationFramework

$xamlFile="\\zeus\a\lrc\GROUPS\WCO\USR\STU_DATA\EXPERT\EXPERT\ServiceDesk_guide\SCript\SD-Tool\MainWindow.xaml"
$inputXAML = Get-Content -Path $xamlFile -Raw
$inputXAML = $inputXAML -replace 'mc:Ignorable="d"',''-replace "x:N","N" -replace '^<Win.*','<Window'
[XML]$XAML =$inputXAML
$reader    = New-Object System.Xml.XmlNodeReader $XAML

try{
    $psform = $psform = [Windows.Markup.XamlReader]::Load($reader)
}catch{
    Write-host $_.Exception
    throw
}

$xaml.SelectNodes("//*[@Name]") | ForEach-Object {
    Try{
        Set-Variable -Name "var_$($_.Name)" -value $psform.FindName($_.Name) -ErrorAction stop
}catch{
    throw
    }
}

Get-Variable var_*


## Enter the search burtton to execute the script
Function OnClick {

#Params
$Computernames    =  Get-Content -Path "\\zeus\a\lrc\GROUPS\WCO\USR\STU_DATA\EXPERT\EXPERT\ServiceDesk_guide\SCript\Data\Computers in a specific workgroup or domain.csv" |    
                     Select-Object -Skip 3 |       
                     ConvertFrom-Csv  -Delimiter ","
$pcNamecsv        =  $Computernames.Where({ $_."Details_Table0_User_Name0" -eq $var_userName.Text})."Details_Table0_Name0" | Where { $_ -like 'BNL0*' -or $_ -like 'BNL5*'}
$script:wwg00m    =  "wwg00m.rootdom.net"
$dedicatedSb      =  "OU=vClientD,OU=AZ-BENE,OU=E2,OU=VDI,OU=Prod,OU=AVC,OU=Clients,DC=wwg00m,DC=rootdom,DC=net"
$Mailboxrights    =  Import-csv  "\\zeus\a\lrc\GROUPS\WCO\USR\STU_DATA\EXPERT\EXPERT\ServiceDesk_guide\SCript\Data\mailboxRights2.csv" -delimiter "|" 

# if hostname >1 search for hostname last used
$ADcomputing = @(
 'Name'
 'LastLogonDate'
 )
$ADproperty    = foreach ($PC in $pcNamecsv)
                 {Get-ADcomputer $PC -Properties $ADcomputing} 
$global:pcName = ($ADproperty | Sort LastLogonDate -Descending | Select -First 1).Name

# Search for IP address with nslookup
$ipAddress     = (nslookup $global:pcName | Select-String Address | Where-Object LineNumber -eq 5).ToString().Split(' ')[-1]

# find properties in Active Directory
$adProperties = @(
        'Name'
        'mail'
        'OfficePhone'
        'LockedOut'
        'Enabled'
        'msDS-UserPasswordExpiryTimeComputed'
        'AccountExpirationDate' 
        'Givenname'
        'DisplayName'
    )

$wwgUser      = Get-ADuser -Identity $var_userName.Text -Server $wwg00m -Properties $adProperties
$agfUser      = Get-ADuser -Identity $var_userName.Text -Properties $adProperties
$agfExpiry    = Get-ADUser -identity $var_userName.Text –Properties  "msDS-UserPasswordExpiryTimeComputed" |
                Select-Object -Property @{Name="ExpiryDate";Expression={[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed")}} | 
                select -expandproperty "ExpiryDate" -first 1 
$wwgExpiry    = Get-ADUser -identity $var_userName.Text -server $wwg00m –Properties  "msDS-UserPasswordExpiryTimeComputed" |
                Select-Object -Property @{Name="ExpiryDate";Expression={[datetime]::FromFileTime($_."msDS-UserPasswordExpiryTimeComputed")}} | 
                select -expandproperty "ExpiryDate" -first 1 

# Ccheck if VPN connection was made before accessing Windows environment                 
$windowsLogon = (Get-Childitem -path \\$pcName\c$\Users\*\AppData\Local\Temp\logon.txt | Sort "LastWriteTime"  -Descending | Select -First 1).LastWriteTime
$vpnLogon     = (Get-ADuser $var_userName.Text -server mgm.agf.be -properties Modified).Modified

# Find properties in Active Directory from nested AVC groups
$SID          = (Get-ADuser $var_userName.Text -Properties SID).SID -replace 'SID',''
$AVCSEC       = (Get-ADGroup -LDAPFilter "(member=CN=$SID,CN=ForeignSecurityPrincipals,DC=wwg00m,DC=rootdom,DC=net)" `
                             -Server wwg00m.rootdom.net -Properties Name).Name 
$DedicatedProperties = @(
                         'Name'
                         'Description')
$dedicatedClients    = Get-ADcomputer -filter * -SearchBase $dedicatedSb -server $wwg00m -properties $DedicatedProperties 
$dedicatedclientName = $dedicatedClients.Where({ $_."Description" -Match $SID})."Name"

# Shared mailbox properties 
$Mailbox   = $Mailboxrights.Where({$_."Trustee" -match $var_userName.Text}) | Select-Object "Display Name", "Email Address",  "Rights","Domain" 

# Computer information, hostname obtained from csv list 
$model            = (Get-WmiObject -Class Win32_ComputerSystem -ComputerName $pcName -ea SilentlyContinue ).Model     
$osVersion        = (Get-WMIObject win32_operatingsystem -ComputerName $pcName -ea SilentlyContinue ).Version  
$osSystem         = (Get-ADcomputer $global:pcName -properties operatingsystem -ea SilentlyContinue ).operatingsystem

# Bitlocker recover key 
$objComputer      =  Get-ADComputer $global:pcName 
$Bitlocker_Object =  Get-ADObject -Filter {objectclass -eq 'msFVE-RecoveryInformation'} -SearchBase $objComputer.DistinguishedName -Properties 'msFVE-RecoveryPassword'
$bitlockerKey     = ($Bitlocker_Object | select msFVE-RecoveryPassword)."msFVE-RecoveryPassword"

             
## Actual binding from Powershell to WPF all info binded with TextBlocks
 
 # Main Tabitem
$var_fullname.Text =    $agfUser.DisplayName
$var_phone.Text    =    $agfUser.OfficePhone
$var_hostname.Text =    $global:pcName
$var_mail.Text     =    $agfUser.mail
$var_enabled.Text  =    $agfUser.enabled                                                                                                                                                                                                                             
$var_exp_acc.Text  =    $agfUser.AccountExpirationDate                                                                                                                                                                                                                                         
$var_exp_pass.Text =    $agfExpiry                                                                                                                                                                                                                                                                                                                                                                                                                                                                          
$var_locked.Text   =    $agfUser.LockedOut

# wwg00m Tabitem
$var_enabled_wwg00m.Text  =    $wwgUser.enabled                                                                                                                                                                                                                             
$var_exp_acc_wwg00m.Text  =    $wwgUser.AccountExpirationDate                                                                                                                                                                                                                                         
$var_exp_pass_wwg00m.Text =    $wwgExpiry                                                                                                                                                                                                                                                                                                                                                                                                                                                                         
$var_locked_wwg00m.Text   =    $wwgUser.LockedOut

# Computer Tabitem
$var_osName.Text   = $osVersion
$var_model.Text    = $model
$var_iAddress.Text = $ipAddress
$var_bkey.Text     = $bitlockerKey                                                                                                                                                                                                         
$var_cName.Text    = $global:pcName
$var_vName.Text    = $osSystem

# Used in more then 1 Tabitem
$var_vpnLog.Text        =    $vpnLogon                                                                                                                                                                                                                              
$var_windowsLog.Text    =    $windowsLogon 
$var_ipCurrent.Text     =    $ipAddress


# AVC Tabitem

$pooledEnv          = If ($AVCSEC -like "AVC-P-POOL-AZBENE-VDIW10*") {
                           $var_Pooled.Text = "Available"}
                    Else { $var_Pooled.Text = "Not available"}
$dedicatedEnv       = If ($AVCSEC -like "AVC-P-POOL-AZBENE-DCW10*") {
                           $var_Dedicated.Text = "Available"}
                    Else { $var_Dedicated.Text = "Not available"}                                        
$uatEnv             = If ($AVCSEC -like "AVC-QS-POOL-AZBENE-VDIW10*") {
                           $var_uat.Text  = "Available"}
                    Else { $var_uat.Text  = "Not available"} 
$pooledRing0        = If ($AVCSEC -like "AVC-P-POOL-AZBENE-RING0-VDIWIN10") {
                           $var_poolRing0.Text ="Available"}
                    Else { $var_poolRing0.Text = "Not available"}
$dedicatedRing0     = If ($AVCSEC -like "AVC-P-POOL-AZBENE-RING0-DEDWIN10") {
                           $var_dedicatedRing0.Text = "Available"}
                    Else { $var_dedicatedRing0.Text = "Not available"}
$softAdmin          = If ($AVCSEC -like "AVC-P-CONF-DCW10-AdminLight"){
                           $var_adminLight.Text = "Available"}
                    Else  {$var_adminLight.text = "Not Available"}
$var_hostNamededicated.Text = $dedicatedclientName


# DataGrid2 Mail TabItem                      
$var_dataGrid2.ItemsSource = $Mailbox


# unlock agf account button
$var_unlockAGF.Add_Click{Unlock-ADAccount -Identity $var_userName.Text 
Start-Sleep -Seconds 2
[System.Windows.MessageBox]::Show('Account has been unlocked')}

## DataGrid and Dropdownlist Tabitem: Access
$var_comboBox.Text = $null
$var_dataGrid.ItemsSource = $null

$var_comboBox.Add_SelectionChanged({

$agfGroups     = (Get-ADuser -identity $var_userName.Text -Properties MemberOf).MemberOf -Replace 'CN=', '' -Replace ',.*', ''
$wwg00mGroups  = (Get-ADuser -identity $var_userName.Text -server $script:wwg00m  -Properties MemberOf).MemberOf -Replace 'CN=', '' -Replace ',.*', ''
$sid           = (Get-ADuser -identity $var_userName.Text -Properties SID).SID -replace 'SID','' 
$avcGroups     = (Get-ADGroup -LDAPFilter "(member=CN=$sid,CN=ForeignSecurityPrincipals,DC=wwg00m,DC=rootdom,DC=net)" `
                                                          -Server $script:wwg00m -Properties Name).Name 


    $item = $var_comboBox.SelectedItem.Content

        if ($item -eq "wwg00m")
            { $var_dataGrid.ItemsSource = $wwg00mGroups

    }elseif
           ($item -eq "agf.be" )
            {$var_dataGrid.ItemsSource  = $agfGroups
}
      else{
           ($item -eq "AVC" )
            $var_dataGrid.ItemsSource   = $avcGroups}
                                   })
}


## OnClick function
$var_OnClick.Add_Click({OnClick $this $_})

## Connect to host with msra "Remote assistance"
$var_connectHost.Add_Click({Start-Process -FilePath "C:\Windows\System32\msra.exe" -Args "/Offerra \\$global:pcName"})

## show GUI
$psform.ShowDialog()  
Toon
  • 29
  • 8
  • For newer version of PowerShell there are several cmdlets to invoke a parallel thread, for Windows PowerShell, see: [Job Event Action with Form not executed](https://stackoverflow.com/a/40808521/1701026). As an aside: [Don't use the `-Variable` cmdlets for dynamic variable names](https://stackoverflow.com/a/68830451/1701026). – iRon Mar 22 '23 at 10:51
  • @iRon thanks for your answer. Seems a lot of examples on how to use these options are difficult to implement in my script. Seems like an endless search to me.. – Toon Mar 22 '23 at 12:46
  • Which command is hanging? There are faster ways to get user detail. Not much way to test this script though wihtout the xml – Scepticalist Mar 22 '23 at 13:45
  • @Scepticalist In fact no command is hanging, the application works as it should be but when executing takes to long it goes on not responding. I'll paste the .xaml file here with a link because the file is to large to put it in the issue description. – Toon Mar 22 '23 at 15:13

1 Answers1

1

I'm not sure which part of your code is hanging, and if Runspaces can solve it.
You can think of runspaces as sandboxes where PowerShell instances execute code.
They can be a separate process, but most of the times, it runs in the process that created it.
Technically a runspace is an entity that can represent a single, or multiple threads, and it's often used for asynchronous operations.

There are great resources out there, but you can't go wrong with the Scripting Guys. There's a whole series about runspaces here.

With that in mind, this is how you quickly set up a runspace:

try {
    <#
        You can't create an object of type System.Management.Automation.Runspaces.Runspace directly.
        Instead you use the static method CreateRunspace() from the RunspaceFactory class.
    #>
    $runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()

    <#
        Apartment state defines the apartment properties for this runspace. The options are:
        STA: Single Thread Apartment
        MTA: Multi Thread Apartment
        Most of the times STA is the way to go.
    #>
    $runspace.ApartmentState = [System.Threading.ApartmentState]::STA

    <#
        Another optional property is the ThreadOptions.
        In this case, we're telling the Runspace to use a new thread.
    #>
    $runspace.ThreadOptions = [System.Management.Automation.Runspaces.PSThreadOptions]::UseNewThread
    $runspace.Open()

    <#
        PowerShell object also cannot be constructed directly.
        You call the Create() static method.
    #>
    $powershell = [System.Management.Automation.PowerShell]::Create()
    $powershell.Runspace = $runspace

    <#
        Scripts here run in a sandbox, meaning it does not have access to the same variables from your script.
        If you want to set variables to your runspace, there are a couple of ways.
        Check out the InitialSessionState.
        https://learn.microsoft.com/en-us/powershell/scripting/developer/hosting/creating-an-initialsessionstate?view=powershell-7.3
    #>
    $powershell.AddScript({
        Write-Output "Do work"
    })

    <#
        The greatest advantage of using runspaces is to run them asynchronously.
        To do so, we call the BeginInvoke(), which returns an IAsyncResult handle.
        We use it to end the invoke.
    #>
    $asyncHandle = $powershell.BeginInvoke()

    <#
        Waiting the work to finish.
        You can put whatever code you need to execute in parallel.
    #>
    while ($powershell.InvocationStateInfo.State -eq 'Running') {
        Start-Sleep -Milliseconds 200
    }

    # Ending invocation.
    $result = $powershell.EndInvoke($asyncHandle)

}
catch {
    <# Build your error handling mechanism here. #>
}
finally {
    <#
        One of the reasons why you cannot instantiate Runspace and PowerShell objects directly is because they instantiate unmanaged resources.
        Unmanaged resources include the thread objects and the memory allocations.
        These objects are not collected by the .NET garbage collector, because the framework does not keep track of what it's still in use.
        To aid that, these objects implement the IDisposable interface.
        Is the developer's responsability to dispose of these objects correctly.
    #>
    $powershell.Dispose()
    $runspace.Dispose()
}

Runspaces can get as complex as you want.
Make sure to go online and educate yourself on the matter.
Hope it helps.

Happy scripting!

FranciscoNabas
  • 505
  • 3
  • 9