Powershell arguments by reference - powershell

My understanding is that a complex object like a hash table is always passed by reference, while simple objects like strings or booleans are passed by value. I have been using this "fact" to do dependency injection with functions, and it has seemed to work until I need to pass the dependency on to a further function.
So I decided to do a simplified test to see where things are going wrong. And now even the dependency injection seems not to work. In this code, my expectation was that since I initially define $state as the return value from Primary in it's "Initialize" mode, I could then simply pass that on to further functions by reference, and when looking at $state at completion I would see all four times, as well as the id.
function Primary {
param (
[parameter(Mandatory=$true,
ParameterSetName = 'initialize')]
[String]$id,
[parameter(Mandatory=$true,
ParameterSetName = 'process')]
[Hashtable]$state
)
if ($id) {
Write-Host 'initialize Primary'
$primary = [Ordered]#{}
$primary.Add('id', $id)
$primary.Add('primaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Secondary -state:$primary -init
} else {
Write-Host 'process Primary'
$primary = $true
$state.Add('primaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Secondary -state:$state
}
return $primary
}
function Secondary {
param (
[parameter(ParameterSetName = 'initialize')]
[Switch]$init,
[parameter(Mandatory=$true)]
[Hashtable]$state
)
if ($init) {
Write-Host 'initialize Secondary'
$state.Add('secondaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
} else {
Write-Host 'process Secondary'
$state.Add('secondaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
}
}
CLS
$state = Primary -id:'Test'
Primary -state:$state
CLS
foreach ($key in $state.Keys) {
Write-Host "$key $($state.$key)"
}
Write-Host
No such luck, I only see the primaryInit time. And yet, in my much more complex program, it seems as if I AM getting $state passed by reference. So, I wonder what is different in this very simple example, that it's not behaving as intended? Or am I misunderstanding what is happening, and some more in my production code I am creating a behavior that I misunderstand to be innate behavior?
I also tried an even more simplified version, to remove the part calling functions from within functions.
function ByReference {
param (
[Hashtable]$state
)
$state.Add('now', (Get-Date))
}
$state = [Ordered]#{
id = 'test'
}
ByReference $state
foreach ($key in $state.Keys) {
Write-Host "$key $($state.$key)"
}
This also shows $state being passed by Value, such that the changes aren't seen when looking at the variable in Main.
EDIT: Based on #Daniel 's link, I revised to
function Primary {
param (
[parameter(Mandatory=$true,
ParameterSetName = 'initialize')]
[String]$id,
[parameter(Mandatory=$true,
ParameterSetName = 'process')]
[Ref]$state
)
if ($id) {
Write-Host 'initialize Primary'
$primary = [Ordered]#{}
$primary.Add('id', $id)
$primary.Add('primaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Secondary -state:$primary -init
} else {
Write-Host 'process Primary'
$primary = $state
$state.Add('primaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Secondary -state:$state
}
return $primary
}
function Secondary {
param (
[parameter(ParameterSetName = 'initialize')]
[Switch]$init,
[parameter(Mandatory=$true)]
[Ref]$state
)
if ($init) {
Write-Host 'initialize Secondary'
$state.Add('secondaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
} else {
Write-Host 'process Secondary'
$state.Add('secondaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
}
}
CLS
$state = Primary -id:'Test'
Primary -state:$state
CLS
foreach ($key in $state.Keys) {
Write-Host "$key $($state.$key)"
}
Write-Host
And this is still not showing anything but the very first time. That said, I can get this to work.
Function Test($data)
{
$data.Test = "New Text"
}
$var = #{}
Test -data $var
$var
Which got me thinking maybe this only works if you aren't using a param() block. So I tried removing the param block and using function Primary ([String]$id, [Ref]$state) {}. Still no joy.
The other thing I notice is that all the examples create the variable in main. I am creating the variable in the init mode of my method. Could it be that the variable needs to be global or script scope? I tried using scope modifiers when initially defining $primary but thiat isn't working either.
EDIT 2: So it seems the key is you must not type the byRef argument.
So this works.
function Test-Primary {
param (
[String]$id,
$state = [Ordered]#{}
)
if ($id) {
Write-Host 'initialize Primary'
$state.Add('id', $id)
$state.Add('primaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Test-Secondary -state:$state -init
return $state
} else {
Write-Host 'process Primary'
$state.Add('primaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Test-Secondary -state:$state
}
}
function Test-Secondary {
param (
[Switch]$init,
$state
)
if ($init) {
Write-Host 'initialize Secondary'
$state.Add('secondaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
} else {
Write-Host 'process Secondary'
$state.Add('secondaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
}
}
CLS
$state = Test-Primary -id:'Test'
Test-Primary -state:$state
CLS
foreach ($key in $state.Keys) {
Write-Host "$key $($state.$key)"
}
Write-Host
A little odd, as I would wish that you could either specifically type the parameter, so ensure you don't change type, but I guess that's just another reason to start using Classes. Functions are "sloppy" because they are intended for down and dirty stuff I guess.
EDIT: OK, progress, but it seems I am still trying to do something odd. I have come to the conclusion that, when doing these multi-"mode" functions where one mode returns a value and the other mode uses a passed by reference variable, you need a different name for the return value. Add to that I need for [Ref] both when calling the function and declaring the variable in the function. I then assumed that I could use [Ref] even with a hash table, as a way to remind myself that this is a by reference value. And then the need for .Value to actually deal with the variable got me to this.
function Test-Primary {
param (
[String]$id,
[Ref]$state
)
if ($id) {
Write-Host 'initialize Primary'
$testPrimary = [Ordered]#{}
$testPrimary.Add('id', $id)
$testPrimary.Add('primaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Test-Secondary -state:([Ref]$testPrimary) -init
return $testPrimary
} else {
Write-Host 'process Primary'
$state.Value.Add('primaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
Test-Secondary -state:$state
}
}
function Test-Secondary {
param (
[Switch]$init,
[Ref]$state
)
if ($init) {
Write-Host 'initialize Secondary'
$state.Value.Add('secondaryInit', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
} else {
Write-Host 'process Secondary'
$state.Value.Add('secondaryProcess', (Get-Date))
Start-Sleep -s:(Get-Random -Minimum 1 -Maximum 5)
}
}
CLS
$state = Test-Primary -id:'Test'
Test-Primary -state:([Ref]$state)
CLS
foreach ($key in $state.Keys) {
Write-Host "$key $($state.$key)"
}
Write-Host

As you figured out hash tables don't need to be passed by reference to change its properties. But to show you how [Ref] actually works, take a look at the following example:
function Test-RefParameter
{
[CmdletBinding()]
param
(
[Parameter(Mandatory = $true)]
[Ref] $Output
)
$Output.Value = "output from test"
}
$testVar = "before test"
Write-Information "Current value: '$testVar'" -InformationAction Continue
Test-RefParameter -Output ([Ref]$testVar)
Write-Information "Current value: '$testVar'" -InformationAction Continue
Which displays:
Current value: 'before test'
Current value: 'output from test'
So the key is to use .Value when assigning the value to the [Ref] parameter

Related

Add Write-Progress to Get-Job/Wait-Job

I'm using the below code to display the results of PowerShell Jobs with a timeout of 120 seconds. I would like to enhance this code by incorporating Write-Progress (based on number of jobs completed). I tried using this example as a reference, however, when I try to incorporate that code, the progress bar displays briefly after all the jobs are all done already.
$Jobs = #()
$ForceStoppedIds = #{}
$Jobs += Get-Job
$Jobs | Wait-Job -Timeout 120 | Out-Null
$Jobs | ?{$_.State -eq 'Running'} | Stop-Job -PassThru | %{$ForceStoppedIds[$_.Id] = $true}
foreach ($Job in $Jobs) {
$Name = $Job.Name
$Output = (Get-Job -Name $Name | Receive-Job)
if ($ForceStoppedIds.Contains($Job.Id)) {
Write-Output "$($Name) - Device unable to process request within 2 minutes"
} else {
Write-Output $Output
}
}
Wait-Job -Timeout 120 will block the thread until the specified timeout or all jobs have completed, hence, is not possible to display progress and wait for them at the same time.
There are 2 alternatives that I can think of, the first one would be to create a proxy command / proxy function around this cmdlet to extend it's functionality.
These blogs demonstrate how to do it:
https://devblogs.microsoft.com/scripting/proxy-functions-spice-up-your-powershell-core-cmdlets/
https://devblogs.microsoft.com/powershell/extending-andor-modifing-commands-with-proxies/
proxy function
You can also follow the indications from this helpful answer.
The other alternative is to define your own function that does a similar work as Wait-Job but, instead of blocking the thread, you can add a loop that will run based on 2 conditions:
That the elapsed time is lower than or equal to the Timeout we passed as argument to the function (we can use Diagnostics.Stopwatch for this).
And, that the jobs are still Running (the $jobs List<T> is still populated).
Note, the function below should work in most cases however is purely for demonstration purposes only and should not be relied upon.
First we define a new function that can be used to display progress as well as wait for our jobs based on a timeout:
using namespace System.Collections.Generic
using namespace System.Diagnostics
using namespace System.Threading
using namespace System.Management.Automation
function Wait-JobWithProgress {
[cmdletbinding()]
param(
[parameter(Mandatory, ValueFromPipeline)]
[object[]] $InputObject,
[parameter()]
[double] $TimeOut
)
begin {
$jobs = [List[object]]::new()
}
process {
foreach($job in $InputObject) {
$jobs.Add($job)
}
}
end {
$timer = [Stopwatch]::StartNew()
$total = $jobs.Count
$completed = 0.1
$expression = { $true }
if($PSBoundParameters.ContainsKey('TimeOut')) {
$expression = { $timer.Elapsed.TotalSeconds -le $TimeOut }
}
while((& $expression) -and $jobs) {
$remaining = $total - $completed
$average = $timer.Elapsed.TotalSeconds / $completed
$estimate = [math]::Round($remaining * $average)
$status = 'Completed Jobs: {0:0} of {1} - ETC: {2}s' -f $completed, $total, $estimate
$progress = #{
Activity = 'Waiting for Jobs'
PercentComplete = $completed / $total * 100
Status = $status
}
Write-Progress #progress
$id = [WaitHandle]::WaitAny($jobs.Finished, 200)
if($id -eq [WaitHandle]::WaitTimeout) {
continue
}
# output this job
$jobs[$id]
# remove this job
$jobs.RemoveAt($id)
$completed++
}
# Stop the jobs not yet Completed and remove them
$jobs | Stop-Job -PassThru | ForEach-Object {
Remove-Job -Job $_
"Job [#{0} - {1}] did not complete on time and was removed." -f $_.Id, $_.Name
} | Write-Warning
Write-Progress #progress -Completed
}
}
Then for testing it, we can create a few jobs with a random timer:
0..10 | ForEach-Object {
Start-Job {
Start-Sleep (Get-Random -Minimum 5 -Maximum 15)
[pscustomobject]#{
Job = $using:_
Result = 'Hello from [Job #{0:D2}]' -f $using:_
}
}
} | Wait-JobWithProgress -TimeOut 10 |
Receive-Job -AutoRemoveJob -Wait | Format-Table -AutoSize

Parallel run for this particular powershell script

I am in the process of re-writing the script below to be able to run in parallel, as can be seen in the code, an array of servers is passed to the script, and then it loads it onto a hash table, loops through each server at a time to do the deployment, for each server there are files to execute in a particular order (see array of files). Looking at the structure, I feel workspace is the way to go here but I could be wrong.
Where the performance gains can be seen in my opinion or having the code such that multiple servers can be executed at thesame time rather than waiting for each server to complete and move onto the next one. foreach parallel
I ran a test to call a function declared outside a workspace, it worked.Is this good practice to call a function declared outside a workspace ? I ask this because I would like to reuse some functions outside the workspace, or is it generally better to put all the code in the workspace even ones that are not intended for parallel workloads i.e one off calls to the code. ?
The below is the code I am testing with.
Function Check-Instance-Connection{
param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
$sql_server,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=1)]
$db_name
)
try
{
#Return extra useful info by using custom objects
$check_outcome = "" | Select-Object -Property log_date, stage, status, error_message
$check_outcome.log_date = (Get-Date)
$check_outcome.stage = 'Ping SQL instance for $sql_server'
#test connection for a sql instance
$connectionstring = "Data Source=$sql_server;Integrated Security =true;Initial Catalog=$db_name;Connect Timeout=5;"
$sqllconnection = New-Object System.Data.SqlClient.SqlConnection $connectionstring
$sqllconnection.Open();
$check_outcome.status = $true
$check_outcome.error_message = ''
return $check_outcome
}
Catch
{
$check_outcome.status = $false
$check_outcome.error_message = $_.Exception.Message
return $check_outcome
}
finally{
$sqllconnection.Close();
}
}
$file_list = #("deployment_1.sql","deployment_2.sql","deployment_3.sql","deployment_4.sql","deployment_5.sql")
$x = (1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)
$k = 'serverid','servername','locationid','appid' # key names correspond to data positions in each array in $x
$h = #{}
For($i=0;$i -lt $x[0].length; $i++){
$x |
ForEach-Object{
[array]$h.($k[$i]) += [string]$_[$i]
}
}
$folder = "F:\Files\"
$database_name = "Test"
foreach ($server_id in $all_server_ids)
{
$severid = $h["serverid"][$all_server_ids.indexof($server_id)]
$servername = $h["servername"][$all_server_ids.indexof($server_id)]
$locationid = $h["locationid"][$all_server_ids.indexof($server_id)]
$message = 'ServerID {0} has a servername of {1} and a location id of {2}' -f $server_id, $h["servername"][$all_server_ids.indexof($server_id)],$h["locationid"][$all_server_ids.indexof($server_id)]
Write-Output $message
Write-Output "This $severid and this $servername and this $locationid"
foreach ($file in $file_list)
{
$is_instance_ok = Check-Instance-Connection $servername $database_name
if ($is_instance_ok.check_outcome -eq $true){
invoke-sqlcmd -ServerInstance "$servername" -inputfile $folder$file -Database "$database_name" -Querytimeout 60 -OutputSqlErrors $true -ConnectionTimeout 10 -ErrorAction Continue -Errorvariable generated_error | Out-Null
}
}
}
Thanks, I did a lot more research and looked at a lot of examples on how workflows work. This is what I have come up with.
Workflow RunExecution
{
Function Check-Instance-Connection{
param
(
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
$sql_server,
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=1)]
$db_name
)
try
{
#Return extra useful info by using custom objects
$check_outcome = "" | Select-Object -Property log_date, stage, status, error_message
$check_outcome.log_date = (Get-Date)
$check_outcome.stage = 'Ping SQL instance for $sql_server'
#test connection for a sql instance
$connectionstring = "Data Source=$sql_server;Integrated Security =true;Initial Catalog=$db_name;Connect Timeout=5;"
$sqllconnection = New-Object System.Data.SqlClient.SqlConnection $connectionstring
$sqllconnection.Open();
$check_outcome.status = $true
$check_outcome.error_message = ''
return $check_outcome
}
Catch
{
$check_outcome.status = $false
$check_outcome.error_message = $_.Exception.Message
return $check_outcome
}
finally{
$sqllconnection.Close();
}
}
$file_list = #("deployment_1.sql","deployment_2.sql","deployment_3.sql","deployment_4.sql","deployment_5.sql")
$x = (1,"server1\DEV3",3,1),(4,"serer1\DEV2",6,2),(3,"serer2\DEV1",4,3)
$k = 'serverid','servername','locationid','appid'
$h = #{}
For($i=0;$i -lt $x[0].length; $i++){
$x |
ForEach-Object{
[array]$h.($k[$i]) += [string]$_[$i]
}
}
$folder = "C:\Temp\"
$database_name = "Test"
$all_server_ids = $h['serverid']
foreach -parallel ($server_id in $all_server_ids)
{
$severid = $h["serverid"][$all_server_ids.indexof($server_id)]
$servername = $h["servername"][$all_server_ids.indexof($server_id)]
$locationid = $h["locationid"][$all_server_ids.indexof($server_id)]
foreach ($file in $file_list)
{
# $check_fine = $is_instance_ok.check_outcome
# if ($check_fine = $true){
invoke-sqlcmd -ServerInstance "$servername" -inputfile $folder$file -Database "$database_name" -Querytimeout 60 -OutputSqlErrors $true -ConnectionTimeout 10 -ErrorAction Continue
write-output "invoke-sqlcmd -ServerInstance $servername -inputfile $folder$file -Database $database_name -Querytimeout 60 -OutputSqlErrors $true -ConnectionTimeout 10 -ErrorAction Continue "
# }
}
}
}
RunExecution

multithread with runspaces instead of foreach cycle

I have a script with a foreach cycle. It has about a dozen functions, each collecting information from remote machines' C$ share (cutting text files, checking file version, etc.)
This is however taking some time, since each machine's data collected after one by one. (sometimes it runs with 500+ input)
Wish to put this into runspaces with parallel execution, but so far no examples worked. I am quite new to the concept.
Current script's outline
$inputfile = c:\temp\computerlist.txt
function 1
function 2
function 3, etc
foreach cycle
function 1
function 2
function 3
All results written to screen with write-host for now.
This example pings a number of server in parallel, so you easily can modify it for your demands:
Add-Type -AssemblyName System.Collections
$GH = [hashtable]::Synchronized(#{})
[System.Collections.Generic.List[PSObject]]$GH.results = #()
[System.Collections.Generic.List[string]]$GH.servers = #('server1','server2','server3');
[System.Collections.Generic.List[string]]$GH.functions = #('Check-Server');
[System.Collections.Generic.List[PSObject]]$jobs = #()
#-----------------------------------------------------------------
function Check-Server {
#-----------------------------------------------------------------
# a function which runs parallel
param(
[string]$server
)
$result = Test-Connection $server -Count 1 -Quiet
$GH.results.Add( [PSObject]#{ 'Server' = $server; 'Result' = $result } )
}
#-----------------------------------------------------------------
function Create-InitialSessionState {
#-----------------------------------------------------------------
param(
[System.Collections.Generic.List[string]]$functionNameList
)
# Setting up an initial session state object
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault()
foreach( $functionName in $functionNameList ) {
# Getting the function definition for the functions to add
$functionDefinition = Get-Content function:\$functionName
$functionEntry = New-Object System.Management.Automation.Runspaces.SessionStateFunctionEntry -ArgumentList $functionName, $functionDefinition
# And add it to the iss object
[void]$initialSessionState.Commands.Add($functionEntry)
}
return $initialSessionState
}
#-----------------------------------------------------------------
function Create-RunspacePool {
#-----------------------------------------------------------------
param(
[InitialSessionState]$initialSessionState
)
$runspacePool = [RunspaceFactory]::CreateRunspacePool(1, ([int]$env:NUMBER_OF_PROCESSORS + 1), $initialSessionState, $Host)
$runspacePool.ApartmentState = 'MTA'
$runspacePool.ThreadOptions = "ReuseThread"
[void]$runspacePool.Open()
return $runspacePool
}
#-----------------------------------------------------------------
function Release-Runspaces {
#-----------------------------------------------------------------
$runspaces = Get-Runspace | Where { $_.Id -gt 1 }
foreach( $runspace in $runspaces ) {
try{
[void]$runspace.Close()
[void]$runspace.Dispose()
}
catch {
}
}
}
$initialSessionState = Create-InitialSessionState -functionNameList $GH.functions
$runspacePool = Create-RunspacePool -initialSessionState $initialSessionState
foreach ($server in $GH.servers)
{
Write-Host $server
$job = [System.Management.Automation.PowerShell]::Create($initialSessionState)
$job.RunspacePool = $runspacePool
$scriptBlock = { param ( [hashtable]$GH, [string]$server ); Check-Server -server $server }
[void]$job.AddScript( $scriptBlock ).AddArgument( $GH ).AddArgument( $server )
$jobs += New-Object PSObject -Property #{
RunNum = $jobCounter++
JobObj = $job
Result = $job.BeginInvoke() }
do {
Sleep -Seconds 1
} while( $runspacePool.GetAvailableRunspaces() -lt 1 )
}
Do {
Sleep -Seconds 1
} While( $jobs.Result.IsCompleted -contains $false)
$GH.results
Release-Runspaces | Out-Null
[void]$runspacePool.Close()
[void]$runspacePool.Dispose()
This would be concurrent and run in about 10 seconds total. The computer could be localhost three times if you got it working.
invoke-command comp1,comp2,comp3 { sleep 10; 'done' }
Simple attempt at api (threads):
$a = [PowerShell]::Create().AddScript{sleep 5;'a done'}
$b = [PowerShell]::Create().AddScript{sleep 5;'b done'}
$c = [PowerShell]::Create().AddScript{sleep 5;'c done'}
$r1 = $a.BeginInvoke(); $r2 = $b.BeginInvoke() $r3 = $c.BeginInvoke()
$a.EndInvoke($r1); $b.EndInvoke($r2); $c.EndInvoke($r3)
a done
b done
c done

Cannot bind argument to parameter 'InputObject' because it is null

I have a powershell script that measures download time on some pages, however I get the error above, I am unsure what I am doing wrong
error is
Cannot bind argument to parameter 'InputObject' because it is null.
function ResponseTime($CommonName,$URL, $environment)
{
$Times = 5
$i = 0
$TotalResponseTime = 0
Write-HOst $URL
While ($i -lt $Times) {
$Request = New-Object System.Net.WebClient
$Request.UseDefaultCredentials = $true
$Start = Get-Date
Write-HOst $URL
$PageRequest = $Request.DownloadString($URL)
$TimeTaken = ((Get-Date) - $Start).TotalMilliseconds
$Request.Dispose()
$i ++
$TotalResponseTime += $TimeTaken
}
$AverageResponseTime = $TotalResponseTime / $i
Write-Host Request to $CommonName took $AverageResponseTime ms in average -ForegroundColor Green
$details = #{
Date = get-date
AverageResponseTime = $AverageResponseTime
ResponseTime = $Destination
Environment = $environment
}
$results += New-Object PSObject -Property $details
$random = Get-Random -minimum 1 -maximum 30
Start-Sleep -s $random
}
#PRODUCTION
ResponseTime -commonname 'app homepage' -URL 'https://url1' -environment 'PRODUCTION'
ResponseTime -commonname 'department homepage' -URL 'https://url2' -environment 'PRODUCTION'
$results | export-csv -Path c:\so.csv -NoTypeInformation
Reviewing your last edit, it seems that $results simply returns $null (As your error says)
The only line setting $results is $results += New-Object PSObject -Property $details
It is not in the scope of your Export-CSV call and - even if it would, $results could be empty, if this line is not called.
You should IMHO set it to e.g. an ArrayList like follows:
$results = New-Object -TypeName System.Collections.ArrayList
And add items to it via
$times = ResponseTime -commonname '' #etc
$results.Add($times) | Out-Null
This gives you an ArrayList - even if there are no items in it - which can easily be transformed to CSV and other formats.
#Clijsters has given the correct answer; i.e. the issue being the scope of your $results variable.
This answer just provides a bit of a code review to help you with other bits going forwards...
function Get-ResponseTime {
[CmdletBinding()]
param (
[Parameter(Mandatory = $true)]
[string]$CommonName
,
[Parameter(Mandatory = $true)]
[string]$URL
,
[Parameter(Mandatory = $true)]
[string]$Environment
,
[Parameter(Mandatory = $false)]
[int]$Times = 5
)
[System.Int64]$TotalResponseTime = 0
[System.Diagnostics.Stopwatch]$stopwatch = New-Object 'System.Diagnostics.Stopwatch'
Write-Verbose "Processing URL: $URL"
1..$times | foreach-object {
[System.Net.WebClient]$Request = New-Object 'System.Net.WebClient'
$Request.UseDefaultCredentials = $true
Write-Verboset "Call $_ to URL: $URL"
$stopwatch.Restart()
$PageRequest = $Request.DownloadString($URL)
$stopwatch.Stop()
$TimeTaken = $stopwatch.Elapsed.TotalMilliseconds
$Request.Dispose()
$TotalResponseTime += $TimeTaken
}
$AverageResponseTime = $TotalResponseTime / $Times
Write-Verbose "Request to $CommonName took $AverageResponseTime ms on average"
$details = #{
Date = get-date
AverageResponseTime = $AverageResponseTime
#ResponseTime = $Destination #this is not declared anywhere / don't know what this field's for
Environment = $environment
}
Write-Output (New-Object 'PSObject' -Property $details)
#do you really want a delay here? Doesn't make much sense... may make sense to include a delay in the above loop; i.e. to stagger your tests?
#$random = Get-Random -minimum 1 -maximum 30
#Start-Sleep -s $random
}
#PRODUCTION
[PSObject[]]$results = #(
(Get-ResponseTime -commonname 'app homepage' -URL 'https://url1' -environment 'PRODUCTION' -Verbose)
,(Get-ResponseTime -commonname 'department homepage' -URL 'https://url2' -environment 'PRODUCTION' -Verbose)
)
$results | Export-Csv -LiteralPath 'c:\so.csv' -NoTypeInformation
Use verb-noun function names (e.g. Get-Item). What is the naming convention for Powershell functions with regard to upper/lower case usage?
Use "Cmdlets" (Advanced Functions) instead of (Basic) Functions; they're basically the same thing, only tagged with [Cmdletbinding()]. The reason for this you get support for functionality such as verbose output. http://www.lazywinadmin.com/2015/03/standard-and-advanced-powershell.html
Use a stopwatch to time processes (you could also use measure-command; but any output would be suppressed / consumed by the measure-command function). Timing a command's execution in PowerShell
Have your cmdlet output its values to the pipeline via Write-Output (or you can leave off the function name; any output caused by placing a variable with nothing to process it will be fed to the pipeline; i.e. write-object $a is the same as a line solely consisting of $a).
Capture the output into your $results variable outside of the function, and handle the results there.

Can you combine CmdletBinding with unbound parameters?

(Powershell 5)
I have the following coalesce function:
(UPDATE: Removed the "optimized" continue call in the process block.)
function Find-Defined {
begin {
$ans = $NULL;
$Test = { $_ -ne $NULL };
}
process {
if ( $ans -eq $NULL ) {
$ans = $_ |? $Test | Select -First 1;
}
}
end {
if ( $ans -ne $NULL ) {
return $ans;
}
else {
$Args `
|% { if ( $_ -is [Array] ) { $_ |% { $_ } } else { $_ } } `
|? $Test `
| Select -First 1 `
| Write-Output `
;
}
}
}
And this works plenty well for me, on command lines like the following:
$NULL, $NULL, 'Legit', 1, 4 | Find-Defined;
$NULL, $NULL | Find-Defined $NULL, #( $NULL, 'Value' ), 3;
$NULL, $NULL | Find-Defined $NULL $NULL 3 4;
You may notice that I encapsulated the decision logic in a ScriptBlock variable. This was because I wanted to parameterize it and I started out trying this.
[CmdletBinding()]param( [ScriptBlock] $Test = { $_ -ne $NULL } );
However, the minute I added CmdletBinding I started to get errors. The binding wanted to try to cast everything in the argument section as a ScriptBlock, so I added
[CmdletBinding(PositionalBinding=$False)]
And then it complained that the unbound arguments couldn't be bound, and so I added:
param( [parameter(Mandatory=$False,Position=0,ValueFromRemainingArguments=$True)][Object[]] $Arguments ...
And whatever I did afterwards added a new error. If I removed the $Test parameter, just localizing it to see what I could do, then I started getting the error I had when developing the first generation:
The input object cannot be bound to any parameters for the command either
because the command does not take pipeline input or the input and its
properties do not match any of the parameters that take pipeline input.
... even though I had a process block.
In the end simply removing the param statement, put it back to its flexible function that I liked.
I still would like to broaden this function to accept both a ScriptBlock test and unbound parameters (as well as Common parameters like -Verbose, if that's possible). That way I could have a general algorithm for string coalescing as well:
$Test = { -not [string]::IsNullOrEmpty( [string]$_ ) };
$NULL,$NULL,'','' | Find-Defined -Test $Test $NULL,'','This should be it' 'Not seen'
Am I missing something?
I believe this solve the problem you are trying to solve, but with a bit of a different implementation. When using CmdletBinding everything must be declared. So you need one parameter for the pipeline input and one for the "unbound" parameters.
Based on you question I wrote these test cases:
Describe 'Find-Defined' {
it 'should retun Legit' {
$NULL, $NULL, 'Legit', 1, 4 | Find-Defined | should be 'Legit'
}
it 'should retun Value' {
$NULL, $NULL | Find-Defined $NULL, #( $NULL, 'Value' ), 3 | should be 'Value'
}
it 'should retun 3' {
$NULL, $NULL | Find-Defined $NULL $NULL 3 4 | should be '3'
}
it 'Should return "This should be it"' {
$Test = { -not [string]::IsNullOrEmpty( [string]$_ ) };
$NULL,$NULL,'','' | Find-Defined -Test $Test $NULL,'','This should be it' 'Not seen' | should be 'This should be it'
}
}
Here is my solution, which passes all of the above cases.
function Find-Defined {
[CmdletBinding()]
param (
[ScriptBlock] $Test = { $NULL -ne $_},
[parameter(Mandatory=$False,ValueFromPipeline =$true)]
[Object[]] $InputObject,
[parameter(Mandatory=$False,Position=0,ValueFromRemainingArguments=$True)]
[Object[]] $Arguments
)
begin {
$ans = $NULL;
function Get-Value {
[CmdletBinding()]
param (
[ScriptBlock] $Test = { $_ -ne $NULL },
[parameter(Mandatory=$False,Position=0,ValueFromRemainingArguments=$True)]
[Object[]] $Arguments,
$ans = $NULL
)
$returnValue = $ans
if($null -eq $returnValue)
{
foreach($Argument in $Arguments)
{
if($Argument -is [object[]])
{
$returnValue = Get-Value -Test $Test -Arguments $Argument -ans $returnValue
}
else
{
if ( $returnValue -eq $NULL ) {
$returnValue = $Argument |Where-Object $Test | Select-Object -First 1;
if($null -ne $returnValue)
{
return $returnValue
}
}
}
}
}
return $returnValue
}
}
process {
$ans = Get-Value -Test $Test -Arguments $InputObject -ans $ans
}
end {
$ans = Get-Value -Test $Test -Arguments $Arguments -ans $ans
if ( $ans -ne $NULL ) {
return $ans;
}
}
}