44#
55
66using 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.
1519Import-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}
0 commit comments