Get Function to process a list

I found this script to produce uptime on a computer. The original script starts at the “Function Get-Uptime {” line and the original $ComputerName variable was set to $ENV:ComputerName.

The script works but I want to process a list of computers. That’s why I changed the $ComputerName variable to $Server and added the first three lines (including the “{” and a final “}”.

The script processes but only displays the last name in my list. It is not processing all the names in my list. How can I get it to process a list instead of just one machine?

$Servers=Get-Content C:\temp\ServersTest.txt
ForEach ($Server in $Servers)
{
Function Get-Uptime {

[CmdLetBinding()]
Param(
[Parameter(ValueFromPipeline = $True, ValueFromPipelineByPropertyName = $True)]
[ValidateNotNullOrEmpty()]
[String]$ComputerName = $Server,

[Int32]$Days = 41,

[Switch]$Ping

)

Begin {
$StartUpID = 6005
$ShutDownID = 6006
$StartingDate = (Get-Date).Date.AddDays(-$Days)
}
Process {

If ($Ping) {
  $PingResult = Test-Connection $ComputerName -Quiet -Count 1
} Else {
  # Fabricate success, we're not running this test
  $PingResult = $True
}

Try {
  $EventLogData = Get-EventLog -LogName System -After $startingDate -Source EventLog -EntryType Information -ComputerName $ComputerName |
    Where-Object { $_.EventID -eq  $StartUpID -Or $_.EventID -eq $ShutDownID }
} Catch { }

# If the previous command was successful (we didn't catch an exception) and $PingResult is true (see the note above about use)
If ($? -And $PingResult) {

  # Initialise the counters
  $Uptime = [TimeSpan]0; $DownTime = [TimeSpan]0

  # To speed up execution or it'll enumerate this collection every time it loops
  $Count = $EventLogData.Count
  # The data should be sorted, but we should not assume that all values are correctly paired
  For ($i = 0; $i -lt $Count; $i++) {

    If ($EventLogData[$i].EventID -eq $StartUpID -And $EventLogData[$i + 1].EventID -eq $ShutDownID) {

      # This is the critical one, sum up all downtime intervals
      $Downtime += New-TimeSpan $EventLogData[$i + 1].TimeGenerated $EventLogData[$i].TimeGenerated
    } ElseIf ($EventLogData[$i].EventID -eq $ShutDownID -And $EventLogData[$i + 1].EventID -eq $StartUpID) {
  
      # Record this, even if it's not used for calculations.
      $Uptime += New-TimeSpan $EventLogData[$i + 1].TimeGenerated $EventLogData[$i].TimeGenerated
    } Else {
      Write-Debug "Could not correlate index $i"
    }
  }

  # Notes on properties:
  # Uptime - An assumed value. Uptime is assumed to be $Days minus any explicit downtime intervals
  # RecordedUptime - Here for information only, not used in calculations as a server that is up all the 
  # time will have no RecodedUptime in the given period.
  # Downtime - From above
  # Percentage - A calculation based on 100% uptime minus each downtime interval
  "" | Select-Object `
    @{n='ComputerName';e={ $ComputerName }},
    @{n='Status';e={ "OK" }},
    @{n='Uptime';e={ (New-TimeSpan -Days $Days) - $DownTime }},
    @{n='RecordedUptime';e={ $Uptime }},
    @{n='Downtime';e={ $Downtime }},
    @{n='Percentage';e={ '{0:P2}' -f (1 - $Downtime.Ticks / (New-TimeSpan -Days $Days).Ticks) }}
} Else {

  # Create a simple return object
  "" | Select-Object `
    @{n='ComputerName';e={ $ComputerName }},
    @{n='Status';e={ If (!$PingResult) { "Ping failed" } Else { $Error[0].Exception.Message.Trim() } }},
    Uptime, RecordedUptime, Downtime, Percentage
}

}
}
}

You don’t need to put your function definition inside a loop like that; you only need calls to the function there. In the function’s param block, the “= $env:COMPUTERNAME” part was just setting a default value that the function will use if the caller doesn’t specify a computer name, but you can override that by passing specific computer names to the function. Here are a few ways that might work:

$Servers=Get-Content C:\temp\ServersTest.txt

# Putting the calls to the function inside a foreach loop
foreach ($Server in $Servers)
{
    Get-Uptime -ComputerName $Server
}

# piping input into the function (if it supports pipeline input; in this case, it does.)
$Servers | Get-Uptime

# Passing the whole array in a single call to the function without pipeline input.
# This won't work with Get-Uptime the way it's currently written, because $ComputerName is
# defined as a [string] instead of a [string[]] array.

Get-Uptime -ComputerName $Servers

Here’s the standard way of writing a function so it can support all three of these calling styles:

function Get-Uptime
{
    # Note that the parameter type is an array
    param (
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]] $ComputerName
    )

    # Functions that accept pipeline input must have a process block to work properly
    process
    {
        # For functions that you want to have support PowerShell 2.0, IF the parameter is allowed
        # to be $null, you need to put a check for $null outside the foreach loop.  Otherwise the
        # loop will execute once with the loop variable set to $null, which is almost certainly
        # not what you want.  This bug is fixed in PowerShell 3.0 and later.

        if ($ComputerName)
        {
            # Because $ComputerName is an array, we need to have a loop inside the process block
            # to handle each value individually.

            foreach ($computer in $ComputerName)
            {
                # Do something with $computer
            }
        }
    }
}