New-DispatchThread.psm1
$internals = @{} $threads = [hashtable]::Synchronized( @{} ) Try { Add-Type -AssemblyName "WindowsBase" } Finally {} function Set-DispatcherFactory { [CmdletBinding()] param( [type] $ReturnType = (& { Try { [System.Windows.Threading.Dispatcher] } Catch { Try { [Avalonia.Threading.Dispatcher] } Catch {} } }), [scriptblock] $Factory = (&{ switch ($ReturnType.ToString()) { "System.Windows.Threading.Dispatcher" { { # For WPF, we don't need to create an App as the encapsulating PowerShell runspace is already an App [System.Windows.Threading.Dispatcher]::CurrentDispatcher [System.Windows.Threading.Dispatcher]::Run() | Out-Null } } "Avalonia.Threading.Dispatcher" { { $App = @{} & { $builder = [Avalonia.AppBuilder]::Configure[Avalonia.Application]() $builder = [Avalonia.AppBuilderDesktopExtensions]::UsePlatformDetect( $builder ) $App.Lifetime = [Avalonia.Controls.ApplicationLifetimes.ClassicDesktopStyleApplicationLifetime]::new() $App.Lifetime.ShutdownMode = [Avalonia.Controls.ShutdownMode]::OnExplicitShutdown $builder = $builder.SetupWithLifetime( $App.Lifetime ) # Return early $App.Instance = $builder.Instance } # Return the UI thread dispatcher [Avalonia.Threading.Dispatcher]::UIThread $App.TokenSource = [System.Threading.CancellationTokenSource]::new() [Avalonia.Controls.DesktopApplicationExtensions]::Run( $App.Instance, $App.TokenSource.Token ) | Out-Null } } Default {} } }) ) Process { If( $null -eq $ReturnType ){ Write-Error "Neither WPF or Avalonia appear to be properly loaded. Please provide a return type!" } Else { If( $null -eq $Factory ){ Write-Error "ReturnType is not a WPF or Avalonia Dispatcher. Please provide a factory scriptblock!" } Else { $internals.dispatcher_class = $ReturnType $internals.factory_script = $Factory } } } } Set-DispatcherFactory function New-DispatchThread{ param( [string] $ThreadName = "Thread", [hashtable] $SessionProxies = @{}, [scriptblock] $Factory = $internals.factory_script ) If( !$ThreadName.Length ){ $ThreadName = "Thread" } # Thread name generator $ThreadName = & { $i = $threads.Keys | Where-Object { $_ -match "^$ThreadName-\d+$" } | Select-String -Pattern "\d+" | ForEach-Object { $_.Matches.Value } | Sort-Object -Descending | Select-Object -First 1 $i = [int]$i $suffix = If( $i ){ "-$( $i+1 )" } Else { If( $threads.Keys -contains $ThreadName ){ "-2" } Else { "" } } "$ThreadName" + "$suffix" } $SessionProxies = [hashtable]::Synchronized( $SessionProxies ) # May want to rewrite this as a (Get-Module).Invoke() call $SessionProxies.Threads = $threads $SessionProxies.ThreadName = $ThreadName $SessionProxies.Factory = $Factory $SessionProxies.DispatcherClass = $internals.dispatcher_class $runspace = [runspacefactory]::CreateRunspace( $Host ) $runspace.ApartmentState = "STA" $runspace.Name = $ThreadName $runspace.ThreadOptions = "ReuseThread" $runspace.Open() | Out-Null foreach( $proxy in $SessionProxies.GetEnumerator() ){ $runspace.SessionStateProxy.PSVariable.Set( $proxy.Name, $proxy.Value ) } $powershell = [powershell]::Create() $powershell.Runspace = $runspace $powershell.AddScript([scriptblock]::Create({ # May want to rewrite this as a (Get-Module).Invoke() call $ThreadController = $Threads[ $ThreadName ] Invoke-Command -ScriptBlock ([scriptblock]::Create( "$( $Factory.ToString() $Factory = $null )")) | ForEach-Object { If( $_.GetType() -eq $DispatcherClass ){ Add-Member -InputObject $ThreadController -MemberType NoteProperty -Name "Dispatcher" -Value $_ -Force } Else { Write-Warning "Dispatcher type incorrect!`nExpected: $DispatcherClass`nGot: $($_.GetType())" } $DispatcherClass = $null } $DispatcherClass = $null $ThreadController.Completed = $true }.ToString())) | Out-Null & { $thread_controller = New-Object PSObject -Property @{ ThreadName = $ThreadName Thread = $powershell Completed = $false } $threads[ $ThreadName ] = $thread_controller # Pre-emptively return the thread controller $thread_controller $InitTask = $powershell.BeginInvoke() # Wait for the thread to initialize While( !$InitTask.IsCompleted -and ![bool]( $thread_controller.psobject.Properties.Name -match "Dispatcher" )){ Start-Sleep -Milliseconds 100 } $thread_controller | Add-Member -MemberType ScriptMethod -Name "Dispose" -Value { $thread_controller = $threads[ $this.ThreadName ] # $thread_controller.Sessions.Values.Dispose() If( $thread_controller.Dispatcher ){ If( $thread_controller.Dispatcher.GetType().ToString() -eq "System.Windows.Threading.Dispatcher" ){ $thread_controller.Dispatcher.InvokeShutdown() } Else { $thread_controller.Invoke({ $Lifetime.Shutdown() }) # Invoke Avalonia Shutdown } } While( !$thread_controller.Completed ){ Start-Sleep -Milliseconds 100 } $thread_controller.Thread.Runspace.Close() $thread_controller.Thread.Runspace.Dispose() $thread_controller.Thread.Dispose() $thread_controller.PSObject.Properties.Remove( "Dispatcher" ) $thread_controller.PSObject.Properties.Remove( "Thread" ) $thread_controller.PSObject.Properties.Remove( "Completed" ) $threads.Remove( $this.ThreadName ) } -Force If( $thread_controller.Dispatcher ){ $thread_controller | Add-Member -MemberType ScriptMethod -Name "Invoke" -Value { param( [parameter(Mandatory = $true)] [scriptblock] $Action, [bool] $Sync = $false ) $Action = [scriptblock]::Create( $Action.ToString() ) $output = New-Object PSObject $output | Add-Member -MemberType ScriptMethod -Name "ToString" -Value { "" } -Force $output | Add-Member -MemberType NoteProperty -Name "Dispatcher" -Value $null -Force If( $Sync ){ # May need to replace with GetMethod("InvokeAsync").MakeGenericMethod([Object[]]) $Result = ($this.Dispatcher.GetType().GetMethods() | Where-Object { $Params = $null If( $_.IsGenericMethod -and ( $_.Name -eq "InvokeAsync" )){ $Params = $_.GetParameters() If( $Params.Count -eq 1 ){ -not( $Params[0].ParameterType.ToString() -like "*.Task*" ) } Else { $false } } Else { $false } }).MakeGenericMethod([Object[]]).Invoke( $this.Dispatcher, @([System.Func[Object[]]]$Action) ) # $Result = $this.Dispatcher.InvokeAsync[Object[]]( $Action ) If ( $Result.Dispatcher.Task ){ # WPF InvokeAsync returns a DispatcherOperation object $output.Dispatcher = $Result.Dispatcher $Result = $Result.Task.GetAwaiter().GetResult() } Else { # Avalonia InvokeAsync returns a Task object $output.Dispatcher = $this.Dispatcher $Result = $Result.GetAwaiter().GetResult() } If( $null -ne $Result ){ $output | Add-Member -MemberType NoteProperty -Name "Result" -Value $null -Force If( $Result.Count -eq 1 ){ $output.Result = $Result[0] } Else { $output.Result = $Result } $output | Add-Member -MemberType ScriptMethod -Name "ToString" -Value { $this.Result.ToString() } -Force } } Else { # $Result = $this.Dispatcher.InvokeAsync( $Action ) $Result = ($this.Dispatcher.GetType().GetMethods() | Where-Object { $Params = $null If( $_.IsGenericMethod -and ( $_.Name -eq "InvokeAsync" )){ $Params = $_.GetParameters() If( $Params.Count -eq 1 ){ -not( $Params[0].ParameterType.ToString() -like "*.Task*" ) } Else { $false } } Else { $false } }).MakeGenericMethod([Object[]]).Invoke( $this.Dispatcher, @([System.Func[Object[]]]$Action) ) If ( $Result.Dispatcher ){ # WPF InvokeAsync returns a DispatcherOperation object $output.Dispatcher = $Result.Dispatcher $output | Add-Member -MemberType NoteProperty -Name "Result" -Value $Result.Task } Else { # Avalonia InvokeAsync returns a Task object $output.Dispatcher = $this.Dispatcher $output | Add-Member -MemberType NoteProperty -Name "Result" -Value $Result } $output | Add-Member -MemberType ScriptMethod -Name "ToString" -Value { $this.Result.ToString() } -Force } $output | Add-Member -MemberType NoteProperty -Name "ThreadName" -Value $this.ThreadName -Force $output | Add-Member -MemberType ScriptMethod -Name "Invoke" -Value { param( [parameter(Mandatory = $true)] [scriptblock] $Action, [bool] $Sync = $false ) $Threads[ $this.ThreadName ].Invoke( $Action, $Sync ) } -Force $output } -Force } # At this point, the thread controller has already been returned } } # Export-ModuleMember -Function New-DispatchThread -Cmdlet New-DispatchThread Export-ModuleMember ` -Function @( "New-DispatchThread", "Set-DispatcherFactory" ) -Cmdlet @( "New-DispatchThread", "Set-DispatcherFactory" ) |