Skip to content

Commit 88dc0d7

Browse files
Add classes to support asynchronous operations (#14)
This change adds private classes and functions that enable the use of traditional .NET async classes and delegates in PowerShell. - Add a utility class for wrapping scriptblocks in delegates that can run in threads without a default runspace. This enables creating types like Task and AsyncCallback from PowerShell. - Add a TaskFactory implementation that creates Tasks from ScriptBlocks
1 parent 9aa11b1 commit 88dc0d7

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed

module/Classes/Async.ps1

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
using namespace System.Collections.Generic
2+
using namespace System.Collections.ObjectModel
3+
using namespace System.Linq.Expressions
4+
using namespace System.Management.Automation
5+
using namespace System.Management.Automation.Runspaces
6+
using namespace System.Threading.Tasks
7+
8+
# Static class that facilitates the use of traditional .NET async techniques in PowerShell.
9+
class AsyncOps
10+
{
11+
static [PSTaskFactory] $Factory = [PSTaskFactory]::new();
12+
static [RunspacePool] $RunspacePool;
13+
14+
# Hides the result property on a Task object. This is done because the getter for Result waits
15+
# for the task to finish, even if just output to the console.
16+
static [Task] HideResult([Task] $target) {
17+
$propertyList = $target.psobject.Properties.Name -notmatch 'Result' -as [string[]]
18+
$propertySet = [PSPropertySet]::new('DefaultDisplayPropertySet', $propertyList) -as [PSMemberInfo[]]
19+
$standardMembers = [PSMemberSet]::new('PSStandardMembers', $propertySet)
20+
21+
$target.psobject.Members.Add($standardMembers)
22+
23+
return $target
24+
}
25+
26+
# Create a delegate from a scriptblock that can be used in threads without runspaces, like those
27+
# used in Tasks or AsyncCallbacks.
28+
static [MulticastDelegate] CreateAsyncDelegate([scriptblock] $function, [type] $delegateType) {
29+
# Create a runspace pool the first time this method is invoked.
30+
if (-not [AsyncOps]::RunspacePool) {
31+
[AsyncOps]::RunspacePool = [runspacefactory]::CreateRunspacePool(1, 4)
32+
[AsyncOps]::RunspacePool.Open()
33+
}
34+
35+
# Create a parameter expression for each parameter the delegate takes.
36+
$parameters = $delegateType.
37+
GetMethod('Invoke').
38+
GetParameters().
39+
ForEach{ [Expression]::Parameter($PSItem.ParameterType, $PSItem.Name) }
40+
41+
# Set AsyncState variable that will hold delegate arguments and/or state.
42+
$preparedScript = 'param($AsyncState) . {{ {0} }}' -f $function
43+
44+
# Prepare variable and constant expressions.
45+
$pool = [Expression]::Property($null, [AsyncOps], 'RunspacePool')
46+
$scriptText = [Expression]::Constant($preparedScript, [string])
47+
$ps = [Expression]::Variable([powershell], 'ps')
48+
$result = [Expression]::Variable([Collection[psobject]], 'result')
49+
50+
# Group the expressions for the body by creating them in a scriptblock.
51+
[Expression[]]$expressions = & {
52+
[Expression]::Assign($ps, [Expression]::Call([powershell], 'Create', @(), @()))
53+
[Expression]::Assign([Expression]::Property($ps, 'RunspacePool'), $pool)
54+
[Expression]::Call($ps, 'AddScript', @(), $scriptText)
55+
56+
foreach ($parameter in $parameters) {
57+
[Expression]::Call($ps, 'AddArgument', @(), $parameter)
58+
}
59+
60+
[Expression]::Assign($result, [Expression]::Call($ps, 'Invoke', @(), @()))
61+
[Expression]::Call($ps, 'Dispose', @(), @())
62+
$result
63+
}
64+
65+
$block = [Expression]::Block([ParameterExpression[]]($ps, $result), $expressions)
66+
$lambda = [Expression]::Lambda(
67+
$delegateType,
68+
$block,
69+
$parameters -as [ParameterExpression[]])
70+
return $lambda.Compile()
71+
}
72+
}
73+
74+
# A TaskFactory implementation that creates tasks that run scriptblocks in a runspace pool.
75+
class PSTaskFactory : TaskFactory[Collection[psobject]] {
76+
# Shortcut to AsyncOps.CreateAsyncDelegate
77+
hidden [MulticastDelegate] Wrap([scriptblock] $function, [type] $delegateType) {
78+
return [AsyncOps]::CreateAsyncDelegate($function, $delegateType)
79+
}
80+
81+
# The remaining functions implement methods from TaskFactory. All of these methods call the base
82+
# method after wrapping the scriptblock to create a delegate that will work in tasks.
83+
[Task[Collection[psobject]]] ContinueWhenAll([Task[]] $tasks, [scriptblock] $continuationAction) {
84+
$delegateType = [Func`2].MakeGenericType([Task[]], [Collection[psobject]])
85+
return [AsyncOps]::HideResult(
86+
([TaskFactory[Collection[psobject]]]$this).ContinueWhenAll(
87+
$tasks,
88+
$this.Wrap($continuationAction, $delegateType),
89+
$this.CancellationToken,
90+
$this.ContinuationOptions,
91+
[TaskScheduler]::Current))
92+
}
93+
94+
[Task[Collection[psobject]]] ContinueWhenAny([Task[]] $tasks, [scriptblock] $continuationAction) {
95+
$delegateType = [Func`2].MakeGenericType([Task[]], [Collection[psobject]])
96+
return [AsyncOps]::HideResult(
97+
([TaskFactory[Collection[psobject]]]$this).ContinueWhenAny(
98+
$tasks,
99+
$this.Wrap($continuationAction, $delegateType),
100+
$this.CancellationToken,
101+
$this.ContinuationOptions,
102+
[TaskScheduler]::Current))
103+
}
104+
105+
[Task[Collection[psobject]]] StartNew([scriptblock] $function) {
106+
return [AsyncOps]::HideResult(
107+
([TaskFactory[Collection[psobject]]]$this).StartNew(
108+
$this.Wrap($function, [Func[Collection[psobject]]]),
109+
$this.CancellationToken,
110+
$this.CreationOptions,
111+
[TaskScheduler]::Current))
112+
}
113+
114+
[Task[Collection[psobject]]] StartNew([scriptblock] $function, [object] $state) {
115+
return [AsyncOps]::HideResult(
116+
([TaskFactory[Collection[psobject]]]$this).StartNew(
117+
$this.Wrap($function, [Func[object, Collection[psobject]]]),
118+
$state,
119+
$this.CancellationToken,
120+
$this.CreationOptions,
121+
[TaskScheduler]::Current))
122+
}
123+
}
124+
125+
function async {
126+
[CmdletBinding()]
127+
param(
128+
[scriptblock]
129+
$ScriptBlock,
130+
131+
[Parameter(ValueFromPipeline)]
132+
[object]
133+
$ArgumentList
134+
)
135+
process {
136+
[AsyncOps]::Factory.StartNew($ScriptBlock, $ArgumentList)
137+
}
138+
}
139+
140+
function await {
141+
[CmdletBinding()]
142+
param(
143+
[Parameter(ValueFromPipeline)]
144+
[Task]
145+
$Task
146+
)
147+
begin {
148+
$taskList = [List[Task]]::new()
149+
}
150+
process {
151+
$taskList.Add($Task)
152+
}
153+
end {
154+
return $taskList.Result
155+
}
156+
}
157+
158+
function ContinueWith {
159+
[CmdletBinding()]
160+
param(
161+
[scriptblock]
162+
$ContinuationAction,
163+
164+
[switch]
165+
$Any,
166+
167+
[Parameter(ValueFromPipeline)]
168+
[Task]
169+
$Task
170+
)
171+
begin {
172+
$taskList = [List[Task]]::new()
173+
}
174+
process {
175+
$taskList.Add($Task)
176+
}
177+
end {
178+
if ($Any.IsPresent) {
179+
return [AsyncOps]::Factory.ContinueWhenAny($taskList, $ContinuationAction)
180+
}
181+
return [AsyncOps]::Factory.ContinueWhenAll($taskList, $ContinuationAction)
182+
}
183+
}

module/EditorServicesCommandSuite.psm1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ if (-not ('Antlr4.StringTemplate.StringRenderer' -as [type])) {
2121

2222
. $PSScriptRoot\Classes\Expressions.ps1
2323
. $PSScriptRoot\Classes\Renderers.ps1
24+
. $PSScriptRoot\Classes\Async.ps1
2425

2526
Get-ChildItem $PSScriptRoot\Public, $PSScriptRoot\Private -Filter '*.ps1' | ForEach-Object {
2627
. $PSItem.FullName

0 commit comments

Comments
 (0)