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"
    )