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()
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!