Skip to content

Commit be85cc9

Browse files
authored
Enhance Push-OutputBinding by using the output binding information (#168)
- Change how `Push-OutputBinding` supports pipeline input: `<# produce values #> | Push-OutputBinding -Name queue` - Change `Push-OutputBinding` to take advantage of the output binding data shared by the worker, to have the `Singleton` and `Collection` behaviors correspondingly.
1 parent f1df4f0 commit be85cc9

File tree

4 files changed

+373
-98
lines changed

4 files changed

+373
-98
lines changed

src/Modules/Microsoft.Azure.Functions.PowerShellWorker/Microsoft.Azure.Functions.PowerShellWorker.psm1

Lines changed: 207 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,26 @@
44
#
55

66
using namespace System.Management.Automation
7+
using namespace System.Management.Automation.Runspaces
8+
using namespace Microsoft.Azure.Functions.PowerShellWorker
79

810
# This holds the current state of the output bindings.
911
$script:_OutputBindings = @{}
12+
$script:_FuncMetadataType = "FunctionMetadata" -as [type]
13+
$script:_RunningInPSWorker = $null -ne $script:_FuncMetadataType
1014
# These variables hold the ScriptBlock and CmdletInfo objects for constructing a SteppablePipeline of 'Out-String | Write-Information'.
1115
$script:outStringCmd = $ExecutionContext.InvokeCommand.GetCommand("Microsoft.PowerShell.Utility\Out-String", [CommandTypes]::Cmdlet)
1216
$script:writeInfoCmd = $ExecutionContext.InvokeCommand.GetCommand("Microsoft.PowerShell.Utility\Write-Information", [CommandTypes]::Cmdlet)
1317
$script:tracingSb = { & $script:outStringCmd -Stream | & $script:writeInfoCmd -Tags "__PipelineObject__" }
1418
# This loads the resource strings.
1519
Import-LocalizedData LocalizedData -FileName PowerShellWorker.Resource.psd1
1620

21+
# Enum that defines different behaviors when collecting output data
22+
enum DataCollectingBehavior {
23+
Singleton
24+
Collection
25+
}
26+
1727
<#
1828
.SYNOPSIS
1929
Gets the hashtable of the output bindings set so far.
@@ -67,29 +77,127 @@ function Get-OutputBinding {
6777
}
6878
}
6979

70-
# Helper private function that sets an OutputBinding.
71-
function Push-KeyValueOutputBinding {
72-
param (
73-
[Parameter(Mandatory=$true)]
74-
[string]
75-
$Name,
80+
# Helper private function that resolve the name to the corresponding binding information.
81+
function Get-BindingInfo
82+
{
83+
[CmdletBinding()]
84+
param(
85+
[Parameter(Mandatory = $true)]
86+
[string] $Name
87+
)
7688

77-
[Parameter(Mandatory=$true)]
78-
[object]
79-
$Value,
89+
if ($script:_RunningInPSWorker)
90+
{
91+
$instanceId = [Runspace]::DefaultRunspace.InstanceId
92+
$bindingMap = $script:_FuncMetadataType::GetOutputBindingInfo($instanceId)
8093

81-
[switch]
82-
$Force
83-
)
94+
# If the instance id doesn't get us back a binding map, then we are not running in one of the PS worker's default Runspace(s).
95+
# This could happen when a custom Runspace is created in the function script, and 'Push-OutputBinding' is called in that Runspace.
96+
if ($null -eq $bindingMap)
97+
{
98+
throw $LocalizedData.DontPushOutputOutsideWorkerRunspace
99+
}
100+
101+
$bindingInfo = $bindingMap[$Name]
102+
if ($null -eq $bindingInfo)
103+
{
104+
$errorMsg = $LocalizedData.BindingNameNotExist -f $Name
105+
throw $errorMsg
106+
}
84107

85-
if (!$script:_OutputBindings.ContainsKey($Name) -or $Force.IsPresent) {
86-
$script:_OutputBindings[$Name] = $Value
87-
} else {
88-
$errorMsg = $LocalizedData.OutputBindingAlreadySet -f $Name
89-
throw $errorMsg
108+
return $bindingInfo
109+
}
110+
}
111+
112+
# Helper private function that maps an output binding to a data collecting behavior.
113+
function Get-DataCollectingBehavior
114+
{
115+
param($BindingInfo)
116+
117+
# binding info not available
118+
if ($null -eq $BindingInfo)
119+
{
120+
return [DataCollectingBehavior]::Singleton
121+
}
122+
123+
switch ($BindingInfo.Type)
124+
{
125+
"http" { return [DataCollectingBehavior]::Singleton }
126+
"blob" { return [DataCollectingBehavior]::Singleton }
127+
128+
"sendGrid" { return [DataCollectingBehavior]::Singleton }
129+
"onedrive" { return [DataCollectingBehavior]::Singleton }
130+
"outlook" { return [DataCollectingBehavior]::Singleton }
131+
"notificationHub" { return [DataCollectingBehavior]::Singleton }
132+
133+
"excel" { return [DataCollectingBehavior]::Collection }
134+
"table" { return [DataCollectingBehavior]::Collection }
135+
"queue" { return [DataCollectingBehavior]::Collection }
136+
"eventHub" { return [DataCollectingBehavior]::Collection }
137+
"documentDB" { return [DataCollectingBehavior]::Collection }
138+
"mobileTable" { return [DataCollectingBehavior]::Collection }
139+
"serviceBus" { return [DataCollectingBehavior]::Collection }
140+
"signalR" { return [DataCollectingBehavior]::Collection }
141+
"twilioSms" { return [DataCollectingBehavior]::Collection }
142+
"graphWebhookSubscription" { return [DataCollectingBehavior]::Collection }
143+
144+
# Be conservative on new output bindings
145+
default { return [DataCollectingBehavior]::Singleton }
90146
}
91147
}
92148

149+
<#
150+
.SYNOPSIS
151+
Combine the new data with the existing data for a output binding with 'Collection' behavior.
152+
Here is what this command do:
153+
- when there is no existing data
154+
- if the new data is considered enumerable by PowerShell,
155+
then all its elements get added to a List<object>, and that list is returned.
156+
- otherwise, the new data is returned intact.
157+
158+
- when there is existing data
159+
- if the existing data is a singleton, then a List<object> is created and the existing data
160+
is added to the list.
161+
- otherwise, the existing data is already a List<object>
162+
- Then, depending on whether the new data is enumerable or not, its elements or itself will also be added to the list.
163+
- That list is returned.
164+
#>
165+
function Merge-Collection
166+
{
167+
param($OldData, $NewData)
168+
169+
$isNewDataEnumerable = [LanguagePrimitives]::IsObjectEnumerable($NewData)
170+
171+
if ($null -eq $OldData -and -not $isNewDataEnumerable)
172+
{
173+
return $NewData
174+
}
175+
176+
$list = $OldData -as [System.Collections.Generic.List[object]]
177+
if ($null -eq $list)
178+
{
179+
$list = [System.Collections.Generic.List[object]]::new()
180+
if ($null -ne $OldData)
181+
{
182+
$list.Add($OldData)
183+
}
184+
}
185+
186+
if ($isNewDataEnumerable)
187+
{
188+
foreach ($item in $NewData)
189+
{
190+
$list.Add($item)
191+
}
192+
}
193+
else
194+
{
195+
$list.Add($NewData)
196+
}
197+
198+
return ,$list
199+
}
200+
93201
<#
94202
.SYNOPSIS
95203
Sets the value for the specified output binding.
@@ -107,46 +215,96 @@ function Push-KeyValueOutputBinding {
107215
.PARAMETER Force
108216
(Optional) If specified, will force the value to be set for a specified output binding.
109217
#>
110-
function Push-OutputBinding {
218+
function Push-OutputBinding
219+
{
111220
[CmdletBinding()]
112221
param (
113-
[Parameter(
114-
Mandatory=$true,
115-
ParameterSetName="NameValue",
116-
Position=0,
117-
ValueFromPipelineByPropertyName=$true)]
118-
[string]
119-
$Name,
120-
121-
[Parameter(
122-
Mandatory=$true,
123-
ParameterSetName="NameValue",
124-
Position=1,
125-
ValueFromPipelineByPropertyName=$true)]
126-
[object]
127-
$Value,
222+
[Parameter(Mandatory = $true, Position = 0)]
223+
[string] $Name,
128224

129-
[Parameter(
130-
Mandatory=$true,
131-
ParameterSetName="InputObject",
132-
Position=0,
133-
ValueFromPipeline=$true)]
134-
[hashtable]
135-
$InputObject,
225+
[Parameter(Mandatory = $true, Position = 1, ValueFromPipeline = $true)]
226+
[object] $Value,
136227

137-
[switch]
138-
$Force
228+
[switch] $Clobber
139229
)
140-
process {
141-
switch ($PSCmdlet.ParameterSetName) {
142-
NameValue {
143-
Push-KeyValueOutputBinding -Name $Name -Value $Value -Force:$Force.IsPresent
230+
231+
Begin
232+
{
233+
$bindingInfo = Get-BindingInfo -Name $Name
234+
$behavior = Get-DataCollectingBehavior -BindingInfo $bindingInfo
235+
}
236+
237+
process
238+
{
239+
$bindingType = "Unknown"
240+
if ($null -ne $bindingInfo)
241+
{
242+
$bindingType = $bindingInfo.Type
243+
}
244+
245+
if (-not $script:_OutputBindings.ContainsKey($Name))
246+
{
247+
switch ($behavior)
248+
{
249+
([DataCollectingBehavior]::Singleton)
250+
{
251+
$script:_OutputBindings[$Name] = $Value
252+
return
253+
}
254+
255+
([DataCollectingBehavior]::Collection)
256+
{
257+
$newValue = Merge-Collection -OldData $null -NewData $Value
258+
$script:_OutputBindings[$Name] = $newValue
259+
return
260+
}
261+
262+
default
263+
{
264+
$errorMsg = $LocalizedData.UnrecognizedBehavior -f $behavior
265+
throw $errorMsg
266+
}
144267
}
145-
InputObject {
146-
$InputObject.GetEnumerator() | ForEach-Object {
147-
Push-KeyValueOutputBinding -Name $_.Name -Value $_.Value -Force:$Force.IsPresent
268+
}
269+
270+
## Key already exists in _OutputBindings
271+
switch ($behavior)
272+
{
273+
([DataCollectingBehavior]::Singleton)
274+
{
275+
if ($Clobber.IsPresent)
276+
{
277+
$script:_OutputBindings[$Name] = $Value
278+
return
279+
}
280+
else
281+
{
282+
$errorMsg = $LocalizedData.OutputBindingAlreadySet -f $Name, $bindingType
283+
throw $errorMsg
148284
}
149285
}
286+
287+
([DataCollectingBehavior]::Collection)
288+
{
289+
if ($Clobber.IsPresent)
290+
{
291+
$newValue = Merge-Collection -OldData $null -NewData $Value
292+
}
293+
else
294+
{
295+
$oldValue = $script:_OutputBindings[$Name]
296+
$newValue = Merge-Collection -OldData $oldValue -NewData $Value
297+
}
298+
299+
$script:_OutputBindings[$Name] = $newValue
300+
return
301+
}
302+
303+
default
304+
{
305+
$errorMsg = $LocalizedData.UnrecognizedBehavior -f $behavior
306+
throw $errorMsg
307+
}
150308
}
151309
}
152310
}

src/Modules/Microsoft.Azure.Functions.PowerShellWorker/PowerShellWorker.Resource.psd1

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
ConvertFrom-StringData @'
77
###PSLOC
8-
OutputBindingAlreadySet=Output binding '{0}' is already set. To override the value, use -Force.
8+
OutputBindingAlreadySet=The output binding '{0}' is already set with a value. The type of the output binding is '{1}'. It only accepts one message/record/file per a Function invocation. To override the value, use -Clobber.
9+
DontPushOutputOutsideWorkerRunspace='Push-OutputBinding' should only be used in the PowerShell Language Worker's default Runspace(s). Do not use it in a custom Runsapce created during the function execution because the pushed values cannot be collected.
10+
BindingNameNotExist=The specified name '{0}' cannot be resolved to a valid output binding of this function.
11+
UnrecognizedBehavior=Unrecognized data collecting behavior '{0}'.
912
###PSLOC
1013
'@

0 commit comments

Comments
 (0)