New-DispatchThread.psm1
$internals = @{} $threads = [hashtable]::Synchronized( @{} ) Try { Add-Type -AssemblyName "WindowsBase" } Finally {} <# .Synopsis A simplistic cmdlet for getting all wrapped threads generated by this module. .Description This cmdlet is used to get all wrapped threads generated by this module. .Example (Get-Threads).47 # Gets the thread controller for the thread with the ManagedThreadId of 47 #> function Get-Threads { [CmdletBinding()] param() Process { $threads } } 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" { switch -Wildcard (Get-Runtime){ "win*" { # Multithreaded Win32DispatcherImpl-based solution { $Dispatcher = & { # Win32 API Window Procedure (WndProc) which we will use to process Win32 API messages $MessageProcessor = & { # Reasons for chunking up the body of $MessageProcessor: # - Allow us to inject the ManagedThreadId into the body $body = @( { param( [System.IntPtr] $hWnd, [uint] $msg, [System.IntPtr] $wParam, [System.IntPtr] $lParam ) # Write-Host "MessageProcessor:" $hWnd $msg $wParam $lParam }.ToString(), # Used to get the dispatcher and dispatcherImpl "`$id = $($ThreadController.Id)", # $win32 { $win32 = @{ "assembly" = [Avalonia.Win32PlatformOptions].Assembly } $win32.unmanaged = $win32.assembly.GetTypes() | Where-Object { $_.Name -eq "UnmanagedMethods" } $win32.WindowsMessage = $win32.unmanaged.GetMember("WindowsMessage") $win32.DispatcherImpl = $win32.assembly.GetTypes() | Where-Object { $_.Name -eq "Win32DispatcherImpl" } $win32.Platform = $win32.assembly.GetTypes() | Where-Object { $_.Name -eq "Win32Platform" } $signals = @{ "WNDCLASSEX" = [Activator]::CreateInstance( ($win32.assembly.GetTypes() | Where-Object { $_.Name -eq "WNDCLASSEX" }) ) "WM_DISPATCH_WORK_ITEM" = ($win32.WindowsMessage.GetEnumValues() | Where-Object { $_.ToString() -eq "WM_USER" })[0].value__ "WM_QUERYENDSESSION" = ($win32.WindowsMessage.GetEnumValues() | Where-Object { $_.ToString() -eq "WM_QUERYENDSESSION" })[0].value__ "WM_SETTINGCHANGE" = ($win32.WindowsMessage.GetEnumValues() | Where-Object { $_.ToString() -eq "WM_WININICHANGE" })[0].value__ "WM_TIMER" = ($win32.WindowsMessage.GetEnumValues() | Where-Object { $_.ToString() -eq "WM_TIMER" })[0].value__ "SignalW" = $win32.DispatcherImpl.GetField( "SignalW", [System.Reflection.BindingFlags]::NonPublic ` -bor [System.Reflection.BindingFlags]::Static ).GetValue($null) # is 0xDEADBEAF "SignalL" = $win32.DispatcherImpl.GetField( "SignalL", [System.Reflection.BindingFlags]::NonPublic ` -bor [System.Reflection.BindingFlags]::Static ).GetValue($null) # is 0x12345678 "TIMERID_DISPATCHER" = $win32.Platform.GetField( "TIMERID_DISPATCHER", [System.Reflection.BindingFlags]::NonPublic ` -bor [System.Reflection.BindingFlags]::Static ).GetValue($null) # is 1 } $conditions = @{ "dispatch" = $msg -eq $signals.WM_DISPATCH_WORK_ITEM ` -and $wParam.ToInt64() -eq $signals.SignalW ` -and $lParam.ToInt64() -eq $signals.SignalL "timer" = $msg -eq $signals.WM_TIMER ` -and $wParam.ToInt64() -eq $signals.TIMERID_DISPATCHER } }.ToString(), # $_dispatcherImpl { $_dispatcherImpl = if( $conditions.dispatch -or $conditions.timer ){ $_threads = if( $Threads ){ $Threads } else { (Get-Module -Name New-DispatchThread).Invoke({ $threads }) } $_dispatcher = $null While( $_dispatcher -eq $null ){ $_dispatcher = ($_threads.GetEnumerator() | Where-Object { $_.Value.Id -eq $id }).Value.Dispatcher if( $_dispatcher -eq $null ){ Start-Sleep -Milliseconds 100 } } & { $_prop = $_dispatcher.GetType().GetField( "_controlledImpl", [System.Reflection.BindingFlags]::NonPublic ` -bor [System.Reflection.BindingFlags]::Instance ) $_prop.GetValue( $_dispatcher ) } } }.ToString(), # Actual body { if ( $conditions.dispatch ){ if( $_dispatcherImpl -ne $null ) { $_dispatcherImpl.DispatchWorkItem() } } if( $conditions.timer ) { if( $_dispatcherImpl -ne $null ) { $_dispatcherImpl.FireTimer() } } # since we don't return anything prior to this, the message is passed to the default message processor return $win32.unmanaged::DefWindowProc($hWnd, $msg, $wParam, $lParam); }.ToString() ) -join "`n" [scriptblock]::create($body) } $constructor = [Avalonia.Threading.Dispatcher]. GetConstructor( [System.Reflection.BindingFlags]::Instance -bor ` [System.Reflection.BindingFlags]::NonPublic, $null, [type[]]@( [Avalonia.Threading.IDispatcherImpl] ), $null ) $win32 = @{ "assembly" = [Avalonia.Win32PlatformOptions].Assembly } $win32.unmanaged = $win32.assembly.GetTypes() | Where-Object { $_.Name -eq "UnmanagedMethods" } $win32.WNDCLASSEX = $win32.assembly.GetTypes() | Where-Object { $_.Name -eq "WNDCLASSEX" } $win32.DispatcherImpl = $win32.assembly.GetTypes() | Where-Object { $_.Name -eq "Win32DispatcherImpl" } # Avalonia.Win32.Interop.UnmanagedMethods+WndProc $win32.WndProc = $win32.assembly.GetTypes() | Where-Object { $_.Name -eq "WndProc" } $win32.MainHandle = $win32.unmanaged::GetModuleHandle($null) $win32.MesageHandle = & { $wnd_class_ex = [Activator]::CreateInstance($win32.WNDCLASSEX) $wnd_class_ex.cbSize = [System.Runtime.InteropServices.Marshal]::SizeOf($wnd_class_ex) $wnd_class_ex.lpfnWndProc = $MessageProcessor -As $win32.WndProc $wnd_class_ex.hInstance = $win32.MainHandle $wnd_class_ex.lpszClassName = "AvaloniaMessageWindow " + [System.Guid]::NewGuid().ToString() $atom = $win32.unmanaged::RegisterClassEx([ref]$wnd_class_ex) if( $atom -eq 0 ) { $global:_error = [System.ComponentModel.Win32Exception]::new() throw $_error } $win32.unmanaged::CreateWindowEx( 0, # dwExStyle $atom, # lpClassName [System.IntPtr]::Zero, # lpWindowName 0, # dwStyle 0, # x 0, # y 0, # nWidth 0, # nHeight [System.IntPtr]::Zero, # hWndParent [System.IntPtr]::Zero, # hMenu [System.IntPtr]::Zero, # hInstance [System.IntPtr]::Zero # lpParam ) } $dispatcher_impl = $win32.DispatcherImpl.GetConstructor([System.IntPtr]).Invoke(@($win32.MesageHandle)) $constructor.Invoke(@($dispatcher_impl)) } $ThreadController | Add-Member -MemberType NoteProperty -Name "CancellationTokenSource" -Value ([System.Threading.CancellationTokenSource]::new()) $Dispatcher $Dispatcher.PushFrame( (& { $constructor = [Avalonia.Threading.DispatcherFrame]. GetConstructor( [System.Reflection.BindingFlags]::Instance -bor ` [System.Reflection.BindingFlags]::NonPublic, $null, [type[]]@( [Avalonia.Threading.Dispatcher], [bool] ), $null ) $Frame = $constructor.Invoke(@($Dispatcher, $true)) $ThreadController.CancellationTokenSource.Token.Register({ $Frame.Continue = $false }) | Out-Null $Frame }) ) } } "osx*" { Write-Warning "Mac OS (Avalonia.Native) is not yet supported" } Default { # Dual-Threaded only UIThread-based solution. Does not work on Mac OS. Untested on Linux { $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 { Write-Error "ReturnType is not a WPF or Avalonia Dispatcher. Please provide a factory scriptblock!" } } }) ) 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 supported Dispatcher. Please provide a factory scriptblock!" } Else { $internals.dispatcher_class = $ReturnType $internals.factory_script = $Factory } } } } Set-DispatcherFactory function New-DispatchThread{ param( [hashtable] $SessionProxies = @{}, [scriptblock] $Factory = $internals.factory_script ) $thread_id = (New-Guid).ToString().ToUpper() -replace "-", "" $SessionProxies = [hashtable]::Synchronized( $SessionProxies ) # May want to rewrite this as a (Get-Module).Invoke() call $SessionProxies.Threads = $threads $SessionProxies.thread_id = $thread_id $SessionProxies.Factory = $Factory $SessionProxies.DispatcherClass = $internals.dispatcher_class $runspace = [runspacefactory]::CreateRunspace( $Host ) $runspace.ApartmentState = "STA" $runspace.Name = $thread_id $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[ $thread_id ] $ThreadController.Id = ([System.Threading.Thread]::CurrentThread.ManagedThreadId) $ThreadController.PowerShell.Runspace.Name = $ThreadController.Id $Threads[ $ThreadController.Id ] = $ThreadController $Threads.Remove( $thread_id ) $thread_id = $null 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 @{ Id = $thread_id PowerShell = $powershell Completed = $false } $threads[ $thread_id ] = $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.Id ] # $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.PowerShell.Runspace.Close() $thread_controller.PowerShell.Runspace.Dispose() $thread_controller.PowerShell.Dispose() $thread_controller.PSObject.Properties.Remove( "Dispatcher" ) $thread_controller.PSObject.Properties.Remove( "Thread" ) $thread_controller.PSObject.Properties.Remove( "Completed" ) $threads.Remove( $this.Id ) } -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*" ) -and ` $Params[0].ParameterType.ToString() -like "*.Func*" } Else { $false } } Else { $false } }).MakeGenericMethod([Object[]]).Invoke( $this.Dispatcher, @([System.Func[Object[]]]$Action) ) # $Result = $this.Dispatcher.InvokeAsync[Object[]]( $Action ) If ( $Result.GetType().Name -eq "DispatcherOperation" ){ # DispatcherOperation object $output.Dispatcher = $Result.Dispatcher If( $Result.Task ){ # WPF DispatcherOperation object $Result = $Result.Task.GetAwaiter().GetResult() } Else { # Avalonia DispatcherOperation object $Result = $Result.GetTask().GetAwaiter().GetResult() } } Else { # 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*" ) -and ` $Params[0].ParameterType.ToString() -like "*.Func*" } Else { $false } } Else { $false } }).MakeGenericMethod([Object[]]).Invoke( $this.Dispatcher, @([System.Func[Object[]]]$Action) ) If ( $Result.GetType().Name -like "*DispatcherOperation*" ){ # DispatcherOperation object $output.Dispatcher = $Result.Dispatcher If( $Result.Task ){ # WPF DispatcherOperation object $output | Add-Member -MemberType NoteProperty -Name "Result" -Value $Result.Task } Else { # Avalonia DispatcherOperation object $output | Add-Member -MemberType NoteProperty -Name "Result" -Value $Result.GetTask() } } Else { # 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 "Id" -Value $this.Id -Force $output | Add-Member -MemberType ScriptMethod -Name "Invoke" -Value { param( [parameter(Mandatory = $true)] [scriptblock] $Action, [bool] $Sync = $false ) $Threads[ $this.Id ].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", "Get-Threads" ) -Cmdlet @( "New-DispatchThread", "Set-DispatcherFactory", "Get-Threads" ) |