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 } } # Internal function for getting the InvokeAsync method function Get-Invoker{ param( [Parameter(Mandatory = $true)] [type] $Type ) $Type.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 } } } function Set-DispatcherFactory { [CmdletBinding()] param( [type] $ReturnType = (& { Try { [System.Windows.Threading.Dispatcher] } Catch { Try { [ThreadExtensions.Dispatcher] } Catch { Try { # Custom Dispatcher Object Add-Type ` -TypeDefinition (Get-Content ` -Path "$PSScriptRoot\ThreadExtensions.cs" ` -Raw) | Out-Null [ThreadExtensions.Dispatcher] } Catch { # a default factory for avalonia is no longer provided, but code to support using one is still maintained # [Avalonia.Threading.Dispatcher] } } } }), [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 $Dispatcher = [System.Windows.Threading.Dispatcher]::CurrentDispatcher <# # WPF's dispatcher is not pausible, so using a token like we do for ThreadExtensions is not possible # - the following script instead shuts down the dispatcher when the token is cancelled $ThreadController | Add-Member ` -MemberType NoteProperty ` -Name "CancellationTokenSource" ` -Value ([System.Threading.CancellationTokenSource]::new()) $ThreadController.CancellationTokenSource.Token.Register(([scriptblock]::Create(@( "`$id = $($ThreadController.Id)", { $ThreadController = $Threads[ $id ] $ThreadController.Dispatcher.InvokeShutdown() }.ToString() ) -join "`n"))) #> $Dispatcher [System.Windows.Threading.Dispatcher]::Run() | Out-Null } } "Avalonia.Threading.Dispatcher" { Write-Warning "Support for Avalonia's dispatcher has been dropped! Please provide your own dispatcher factory scriptblock!" } "ThreadExtensions.Dispatcher" { { $Dispatcher = [ThreadExtensions.Dispatcher]::new() $ThreadController | Add-Member ` -MemberType NoteProperty ` -Name "CancellationTokenSource" ` -Value ([System.Threading.CancellationTokenSource]::new()) $Dispatcher $Dispatcher.Run( $ThreadController.CancellationTokenSource.Token ) } } } }) ) Process { If( ($null -eq $ReturnType) -or -not (Get-Invoker $ReturnType) ){ Write-Error "ReturnType is not a supported Dispatcher. Please provide a dispatcher with an InvokeAsync<TReturn>( Func<TReturn> ) method!" } Else { If( $null -eq $Factory ){ Write-Error "ReturnType does not have a default factory! 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.CancellationTokenSource ){ $thread_controller.CancellationTokenSource.Cancel() $thread_controller.CancellationTokenSource.Dispose() } If( $thread_controller.Dispatcher ){ If( $thread_controller.Dispatcher.InvokeShutdown ){ $thread_controller.Dispatcher.InvokeShutdown() } If( $thread_controller.Dispatcher.Dispose ){ $thread_controller.Dispatcher.Dispose() } } 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 ){ $Result = (Get-Invoker $this.Dispatcher.GetType()). 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" ) |