New-ThreadController.psm1
$internals = @{} $threads = [hashtable]::Synchronized( @{} ) Try { Add-Type ` -TypeDefinition (Get-Content ` -Path "$PSScriptRoot\ThreadExtensions.cs" ` -Raw) | Out-Null } Catch { throw [System.Exception]::new( "Failed to load ThreadExtensions.cs!", $_.Exception ) } Try { Add-Type -AssemblyName "WindowsBase" } Catch {} <# .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 Update-DispatcherFactory { [CmdletBinding()] param( [type] $ReturnType = (& { Try { [System.Windows.Threading.Dispatcher] } Catch { Try { [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(@( "`$_name = $ThreadName", { $ThreadController = $Threads[ $_name ] $ThreadController.Dispose() }.ToString() ) -join "`n"))) #> $Dispatcher [System.Windows.Threading.Dispatcher]::Run() | Out-Null } } "ThreadExtensions.Dispatcher" { { $Dispatcher = [ThreadExtensions.Dispatcher]::new() $ThreadController | Add-Member ` -MemberType NoteProperty ` -Name "CancellationTokenSource" ` -Value ([System.Threading.CancellationTokenSource]::new()) $Dispatcher $Dispatcher.Run( $ThreadController.CancellationTokenSource.Token ) } } "Avalonia.Threading.Dispatcher" { Write-Warning "Support for Avalonia's dispatcher has been dropped! Please provide your own dispatcher factory scriptblock!" } } }) ) 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 } } } } Update-DispatcherFactory function New-ThreadController{ param( [string] $Name, [hashtable] $SessionProxies = @{}, [scriptblock] $Factory = $internals.factory_script ) $guid = $null if( (-not $Name) -or ` ($Name.Trim() -eq "") -or ` ($Name -eq "Anonymous") ){ $guid = ((New-Guid).ToString().ToUpper() -replace "-", "") If( $Name -eq "Anonymous" ){ $Name = "Anonymous-$guid" $guid = $null } Else { $Name = "BadThread-$guid" } } if( $threads[ $Name ] ){ throw [System.ArgumentException]::new( "Named thread $Name already exists!", "Name" ) } $SessionProxies = [hashtable]::Synchronized( $SessionProxies ) # May want to rewrite this as a (Get-Module).Invoke() call $SessionProxies.Threads = $threads If( $guid ){ $SessionProxies.guid = $guid } $SessionProxies.ThreadName = $Name $SessionProxies.Factory = $Factory $SessionProxies.DispatcherClass = $internals.dispatcher_class $runspace = [runspacefactory]::CreateRunspace( $Host ) $runspace.ApartmentState = "STA" $runspace.Name = $Name $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 ] $ThreadController | Add-Member ` -MemberType NoteProperty ` -Name "Id" ` -Value ([System.Threading.Thread]::CurrentThread.ManagedThreadId) If( $guid ){ $ThreadName = "ManagedThreadId-$( $ThreadController.Id.ToString() )" $ThreadController.Name = $ThreadName $ThreadController.PowerShell.Runspace.Name = $ThreadName $Threads[ $ThreadName ] = $ThreadController $Threads.Remove( "BadThread-$guid" ) $guid = $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 @{ Name = $Name PowerShell = $powershell Completed = $false } $threads[ $Name ] = $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.Name ] # $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" ) $threads.Remove( $this.Name ) } -Force If( $thread_controller.Dispatcher ){ $thread_controller | Add-Member -MemberType ScriptMethod -Name "Invoke" -Value { param( [parameter(Mandatory = $true)] $Action, [bool] $Sync = $false ) if( $Action.GetType().Name -eq "ScriptBlock" ){ $Action = [scriptblock]::Create( $Action.ToString() ) } Elseif( $Action.GetType().Name -eq "String" ){ Try { $Action = [scriptblock]::Create( $Action ) } Catch { throw [System.ArgumentException]::new( "Action must be a ScriptBlock or Valid ScriptBlock String!", "Action" ) } } Else { throw [System.ArgumentException]::new( "Action must be a ScriptBlock or Valid ScriptBlock String!", "Action" ) } $output = New-Object PSObject $output | Add-Member -MemberType ScriptMethod -Name "ToString" -Value { "" } -Force $output | Add-Member -MemberType NoteProperty -Name "Dispatcher" -Value $null -Force $Result = Try { (Get-Invoker $this.Dispatcher.GetType()). MakeGenericMethod([Object[]]). Invoke( $this.Dispatcher, @( [System.Func[Object[]]]$Action ) ) } Catch { throw "Problem with Get-Invoker call: $_" } If( $null -eq $Result ){ throw "Problem with Get-Invoker call: Result is null!" } Try { If( $Sync ){ # $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 ) 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 } } Catch { throw "Problem with parsing output: $_" } $output | Add-Member -MemberType NoteProperty -Name "Name" -Value $this.Name -Force $output | Add-Member -MemberType NoteProperty -Name "Id" -Value $this.Id -Force $output | Add-Member -MemberType ScriptMethod -Name "Invoke" -Value { param( [parameter(Mandatory = $true)] $Action, [bool] $Sync = $false ) switch ($Action.GetType().Name) { "ScriptBlock" {} "String" {} default { throw [System.ArgumentException]::new( "Action must be a ScriptBlock or String!", "Action" ) } } Try { $Threads[ $this.Name ].Invoke( $Action, $Sync ) } Catch { if( $_.Exception.Message -like "*null-valued expression*" ){ throw [System.Exception]::new( "Thread controller does not exist or was disposed!", $_.Exception ) } Else { throw $_ } } } -Force $output } -Force } # At this point, the thread controller has already been returned } } function Async { param( [parameter(Mandatory = $true)] $Action, $Thread, [switch] $Sync ) $dispose = If( $null -eq $Thread ){ $Thread = "Anonymous" $true } Else { $false } If( $Thread.GetType() -eq [string] ){ if( $Threads[ $Thread ] ){ $Thread = $threads[ $Thread ] } Else { $Thread = New-ThreadController -Name $Thread } } Else { $Thread = $threads[ $Thread.Name ] } if( $null -eq $Thread ){ throw [System.ArgumentException]::new( "Thread must be a new or existing Thread Name or an Existing ThreadController!", "Thread" ) } switch ($Action.GetType().Name) { "ScriptBlock" {} "String" {} default { throw [System.ArgumentException]::new( "Action must be a ScriptBlock or String!", "Action" ) } } Try { $Thread.Invoke( $Action, $Sync ) } Catch { if( $_.Exception.Message -like "*null-valued expression*" ){ throw [System.Exception]::new( "Thread controller does not exist or was disposed!", $_.Exception ) } Else { throw $_ } } If( $dispose ){ $Thread.Dispose() } } Export-ModuleMember -Function @( "New-ThreadController", "Update-DispatcherFactory", "Get-Threads", "Async" ) |