UcsMainUtil.psm1

Function Get-UcsCleanJSON
{
  <#
      .SYNOPSIS
      Takes a string intended for a JSON string, sanitizes it, and returns the result.
  #>


  Param (
    [Parameter(Mandatory,HelpMessage = 'Add help message for user',ValueFromPipeline)][String]$String
  )

  $ThisString = $String

  $ThisString = $ThisString.Replace('\','\\') #Escape slashes first since they're used for escape characters.
  $ThisString = $ThisString.Replace("`"","\`"") #Escape any double quotes.

  Return $ThisString
}


Function Test-UcsIsAdministrator
{
  <#
      .SYNOPSIS
      Returns if the current powershell session has administrator rights.
  #>

  $user = [Security.Principal.WindowsIdentity]::GetCurrent()

  Return (New-Object -TypeName Security.Principal.WindowsPrincipal -ArgumentList $user).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator)
}


Function Test-UcsPolycomRootCertificate
{
  <#
      .SYNOPSIS
      Tests for the presence of the Polycom Root certificate in the certificates store.
  #>

  $MachineCertificates = Get-ChildItem -Path Cert:\LocalMachine\Root
  $UserCertificates = Get-ChildItem -Path Cert:\CurrentUser\Root
  $AllCertificates = $MachineCertificates + $UserCertificates | Where-Object -Property Subject -Like -Value 'CN=Polycom*'

  if($AllCertificates.count -gt 0)
  {
    Return $true
  }
  else
  {
    Return $false
  }
}

Function Add-UcsHost
{
  <#
      .SYNOPSIS
      Adds an entry to the system's hosts file.
  #>

  Param (
    [Parameter(Mandatory,HelpMessage = 'Add help message for user')][string]$IPv4Address,
    [Parameter(Mandatory,HelpMessage = 'Add help message for user')][string]$Hostname
  )
  $Filename = "$env:windir\System32\drivers\etc\hosts"
  Remove-UcsHost -Hostname $Hostname
  $IPv4Address + "`t`t" + $Hostname | Out-File -Encoding ASCII -Append -FilePath $Filename
}

Function Remove-UcsHost
{
  <#
      .SYNOPSIS
      Removes an entry from the system's hosts file.
  #>

  Param(
    [Parameter(Mandatory,HelpMessage = 'Add help message for user')][string]$Hostname
  )
  $Filename = "$env:windir\System32\drivers\etc\hosts"
  $c = Get-Content -Path $Filename
  $newLines = @()

  foreach ($line in $c)
  {
    $bits = [regex]::Split($line, '\t+')
    if ($bits.count -eq 2)
    {
      if ($bits[1] -ne $Hostname)
      {
        $newLines += $line
      }
    }
    else
    {
      $newLines += $line
    }
  }

  # Write file
  Clear-Content -Path $Filename
  foreach ($line in $newLines)
  {
    $line | Out-File -Encoding ASCII -Append -FilePath $Filename
  }
}


Function Convert-UcsUptimeString
{
  <#
      .SYNOPSIS
      Takes a UCS API formatted uptime string and returns a timespan. (0 day 0:55:11)
 
      .DESCRIPTION
      Add a more complete description of what the function does.
 
      .PARAMETER Uptime
      A timespan in string format, formatted in Polycom Unified Communications Software format - such as "2 days 12:44:11." Allows input of "day" or "days" and additionally for the hours to be reprsented as a single number without a leading 0.
 
      .OUTPUTS
      A timespan object.
  #>


  Param([Parameter(Mandatory,HelpMessage = 'Add help message for user')][ValidatePattern('^\d+( Day(s)? )|(\.)\d{1,2}:\d{2}:\d{2}$')][String]$Uptime)

  if($Uptime -like "*Day*") {
    $UptimeFirstSpace = $Uptime.IndexOf(' ')
    $UptimeDays = $Uptime.Substring(0,$UptimeFirstSpace)
  } else {
    #Format is more like 3.02:24:46, which is weird but sometimes happens.
    $null = $Uptime -match '^\d+'
    $UptimeDays = $Matches[0]
  }

  $UptimeThisIndex = $Uptime.Length - 1 #End of string
  $UptimeSeconds = $Uptime.Substring(($UptimeThisIndex - 1),2)
  $UptimeThisIndex = $UptimeThisIndex - 3
  $UptimeMinutes = $Uptime.Substring(($UptimeThisIndex - 1),2)
  $UptimeThisIndex = $UptimeThisIndex - 3
  $UptimeHours = $Uptime.Substring(($UptimeThisIndex - 1),2).Trim(' ')

  $UptimeTimeSpan = New-TimeSpan -Days $UptimeDays -Hours $UptimeHours -Minutes $UptimeMinutes -Seconds $UptimeSeconds

  Return $UptimeTimeSpan
}


Function Get-UcsStatusCodeString
{
  <#
      .SYNOPSIS
      Turns a Polycom status code into a stringified description of what it represents. Optionally allows the user to include IPv4 address and Endpoint to allow return of additional information.
      .PARAMETER IPv4Address
      The network address in IPv4 notation, such as 192.123.45.67.
  #>

  Param([Parameter(Mandatory,HelpMessage = 'One or more status codes to get a value for.',ValueFromPipelineByPropertyName,ValueFromPipeline)][int[]]$StatusCode,
    [ValidatePattern('^([0-2]?[0-9]{1,2}\.){3}([0-2]?[0-9]{1,2})$')][String]$IPv4Address,
  [String]$ApiEndpoint)

  BEGIN
  {

  }
  PROCESS
  {
    Foreach ($ThisStatusCode in $StatusCode)
    {
      $Result = New-Object PsCustomObject
      $Result | Add-Member -MemberType NoteProperty -Name StatusCode -Value $ThisStatusCode
      $Result | Add-Member -MemberType NoteProperty -Name ResponseOK -Value $false
      $Result | Add-Member -MemberType NoteProperty -Name StatusString -Value 'Unknown status code.'
      $Result | Add-Member -MemberType NoteProperty -Name Exception -Value $null

      if($null -eq $ThisStatusCode)
      {
        $Result.StatusString = 'No response returned from API.'
        $Result.Exception = New-Object System.Runtime.InteropServices.ExternalException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 2000)
      {
        $Result.ResponseOK = $true
        $Result.StatusString = 'API executed successfully.'
      }
      elseif($ThisStatusCode -eq 4000)
      {
        $Result.StatusString = 'Invalid input parameters.'
        $Result.Exception = New-Object System.ArgumentException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 4001)
      {
        $Result.StatusString = 'Device busy.'
        $Result.Exception = New-Object System.Runtime.InteropServices.ExternalException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 4002)
      {
        $Result.StatusString = 'Line not registered.'
        $Result.Exception = New-Object System.InvalidOperationException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 4003)
      {
        $Result.StatusString = 'Operation not allowed.'
        $Result.Exception = New-Object System.InvalidOperationException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 4004)
      {
        $Result.StatusString = 'Operation not supported.'
        $Result.Exception = New-Object System.InvalidOperationException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 4005)
      {
        $Result.StatusString = 'Line does not exist.'
        $Result.Exception = New-Object System.InvalidOperationException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 4006)
      {
        $Result.StatusString = 'URLs not configured.'
        $Result.Exception = New-Object System.InvalidOperationException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 4007)
      {
        $Result.ResponseOK = $true
        $Result.StatusString = 'Call does not exist.'
        $Result.Exception = New-Object System.NullReferenceException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 4008)
      {
        $Result.StatusString = 'Configuration export failed.'
        $Result.Exception = New-Object System.Runtime.InteropServices.ExternalException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 4009)
      {
        $Result.StatusString = 'Input size limit exceeded.'
        $Result.Exception = New-Object System.InvalidOperationException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 4010)
      {
        $Result.StatusString = 'Default password not allowed.'
        $Result.Exception = New-Object System.InvalidOperationException -ArgumentList $Result.StatusString
      }
      elseif($ThisStatusCode -eq 5000)
      {
        $Result.StatusString = 'Failed to process request.'
        $Result.Exception = New-Object System.Runtime.InteropServices.ExternalException -ArgumentList $Result.StatusString
      }
      else
      {
        $Result.StatusString = "Unknown error $StatusCode occurred."
        $Result.Exception = New-Object System.Runtime.InteropServices.ExternalException -ArgumentList $Result.StatusString
      }

      if($ApiEndpoint)
      {
        $Result | Add-Member -MemberType NoteProperty -Name ApiEndpoint -Value $ApiEndpoint
      }
      if($IPv4Address)
      {
        $Result | Add-Member -MemberType NoteProperty -Name IPv4Address -Value $IPv4Address
      }

      $Result
    }
  }
  END
  {
  }
}

Function Test-UcsSkypeModuleIsAvailable
{
  $Modules = ('Lync', 'SkypeForBusiness')
  $ReturnValue = $false

  Foreach($Module in $Modules)
  {
    if(Get-Module -ListAvailable | Where-Object -FilterScript {
        $_.Name -eq $Module
    })
    {
      Write-Debug -Message ('{0} is available on this system.' -f $Module)
      $ReturnValue = $true

      if(Get-Module -Name $Module)
      {
        Write-Debug -Message ('{0} module is loaded and ready to use.' -f $Module)
      }
      else
      {
        Write-Debug -Message ('{0} module is available but unloaded. Now importing.' -f $Module)
        Import-Module -Name $Module
      }
    }
  }

  Return $ReturnValue
}

Function New-UcsLog
{
  Param([Parameter(Mandatory,ValueFromPipeline)][String[]]$LogString,
    [Parameter(Mandatory)][String][ValidateSet('app','boot')]$LogType,
    [Parameter(ValueFromPipelineByPropertyName)][ValidatePattern('^([0-2]?[0-9]{1,2}\.){3}([0-2]?[0-9]{1,2})$')][String]$IPv4Address = "",
  [Parameter(ValueFromPipelineByPropertyName)][ValidatePattern('^[a-f0-9]{12}$')][String]$MacAddress = "")
  BEGIN
  {

  }
  PROCESS
  {
      $SplitString = $LogString.Split("`n") | Where-Object -FilterScript {$_.Length -gt 2 }
      Foreach ($Line in $SplitString)
      {
        Try
        {
          $SplitLine = $Line.Split('|')
          $Message = $SplitLine[4..(($Splitline.Count)-1)] -Join "|" #After the 4th pipe, sometimes the log has additional pipes just to break parsing.

          $RawTime = $SplitLine[0]
          if($RawTime -match '\d+\.\d{3}')
          {
            #This is a time since boot.
            $Datetime = $null
            $TimeSinceBoot = New-TimeSpan -Seconds $RawTime
          }
          else
          {
            #This is actual time.
            #MMDDHHMMSS
            $TimeSinceBoot = $null
            Try
            {
              $Datetime = Get-Date -Month $RawTime.Substring(0,2) -Day $RawTime.Substring(2,2) -Hour $RawTime.Substring(4,2)  -Minute $RawTime.Substring(6,2) -Second $RawTime.Substring(8,2) -Millisecond 0 -ErrorAction Stop
            }
            Catch
            {
              Write-Debug "Invalid datetime detected: $RawTime"
              $Datetime = $null
            }
            if($Datetime -gt (Get-Date))
            {
              $Datetime = $Datetime.AddYears(-1) #because the string doesn't specify a year, we need to correct it if it's in the future.
            }

            if($LogType -eq 'boot') {
              #Boot times are universal time, not local time.
              $Datetime = $Datetime + (($Datetime)-($Datetime).ToUniversalTime())
            }
          }

          $ThisOutput = New-Object PsCustomObject
          $ThisOutput | Add-Member -MemberType NoteProperty -Name RawTime -Value $SplitLine[0]
          $ThisOutput | Add-Member -MemberType NoteProperty -Name DateTime -Value $Datetime
          $ThisOutput | Add-Member -MemberType NoteProperty -Name TimeSinceBoot -Value $TimeSinceBoot
          $ThisOutput | Add-Member -MemberType NoteProperty -Name Id -Value $SplitLine[1].Trim(' ')
          $ThisOutput | Add-Member -MemberType NoteProperty -Name Level -Value $SplitLine[2]
          $ThisOutput | Add-Member -MemberType NoteProperty -Name MissedEvents -Value $SplitLine[3]
          $ThisOutput | Add-Member -MemberType NoteProperty -Name Message -Value $Message
          $ThisOutput | Add-Member -MemberType NoteProperty -Name LogType -Value $LogType

          if($IPv4Address.length -ge 7) {
            $ThisOutput | Add-Member -MemberType NoteProperty -Name IPv4Address -Value $IPv4Address
          }
          if($MacAddress.length -eq 12) {
            $ThisOutput | Add-Member -MemberType NoteProperty -Name MacAddress -Value $MacAddress
          }

          $ThisOutput

        } Catch
        {
          Write-Debug -Message "Skipped $Line due to error $_"
        }
        }
  }
  END
  {

  }

}

Function Convert-UcsVersionNumber
{
  Param([Parameter(Mandatory,ValueFromPipeline)][ValidatePattern('(\d+[A-Z]?\.){3}\d{4,}[A-Z]*(\s.+)?')][String]$FirmwareRelease)

  $Success = $FirmwareRelease -match "(?<major>\d+)\.(?<minor>\d+)\.(?<build>\d+[A-Z]?)\.(?<revision>\d+[A-Z]*)(?<notes>\s.+)?"

  if($Success)
  {
    $OutputResult = 1 | Select-Object @{Name="FirmwareRelease";Expression={$FirmwareRelease}},@{Name="Major";Expression={$Matches['major']}},@{Name="Minor";Expression={$Matches['minor']}},@{Name="Build";Expression={$Matches['build']}},@{Name="Revision";Expression={$Matches['revision']}},@{Name="Note";Expression={($Matches['notes']).Trim()}}
    Return $OutputResult
  }
  else
  {
    Write-Error "Couldn't parse firmware version $FirmwareRelease" -Category InvalidData
  }
}

Function Get-UcsUnixTime
{
  $UnixTime = [Math]::Round( (((Get-Date) - (Get-Date -Date 'January 1 1970 00:00:00.00')).TotalSeconds), 0)
  Return $UnixTime
}

Function New-UcsCallObject
{
  Param(`
    [String][ValidatePattern('^(0x)?[a-f0-9]{7,8}$')]$CallHandle = $null,
    [ValidateSet('','Incoming','Outgoing','Missed','Placed','Received','In','Out')][String]$Type = $null,
    [ValidateSet('','Conference','Normal','Rejected','RemotelyHandled','Transferred','Busy','UserForwarded','Partial')][String]$Disposition = $null,
    [String[]]$RemotePartyName = $null,
    [String[]]$RemotePartyNumber = $null,
    [String[]]$LocalPartyName = $null,
    [String[]]$LocalPartyNumber = $null,
    [String[]]$ConnectionName = $null,
    [String[]]$ConnectionNumber = $null,
    [ValidateSet('Dialtone','Connected','CallHold','Hold','Setup','RingBack','Offering','Log','Proceeding','')][String]$CallState = $null,
    [ValidateSet('SIP','')][String]$Protocol = $null,
    [Nullable[DateTime]]$StartTime = $null,
    [Nullable[TimeSpan]]$Duration = $null,
    [ValidateRange(0,100)][Int]$LineID = -1,
    [ValidateRange(0,100)][Int]$CallSequence = -1,
    [ValidatePattern('^(\d{1,2}\*?)?$')][String]$UIAppearanceIndex = $null,
    [Nullable[Bool]]$ActiveCall = $null,
    [Nullable[Bool]]$Ringing = $null,
    [Nullable[Bool]]$Muted = $null,
    [ValidateRange(0,65535)][Int]$RTPPort = -1,
    [ValidateRange(0,65535)][Int]$RTCPPort = -1,
    [ValidatePattern('^[a-f0-9]{12}$')][String]$MacAddress = $null,
    [Parameter(HelpMessage = '127.0.0.1',ValueFromPipelineByPropertyName,ValueFromPipeline)][ValidatePattern('^([0-2]?[0-9]{1,2}\.){3}([0-2]?[0-9]{1,2})$')][String]$IPv4Address = $null,
    [Switch]$ExcludeNullProperties,
    [Switch]$IsLog ` #For logs, we don't want to compute based on current time.
  )

  $ThisOutputCall = New-Object -TypeName PSObject
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name CallHandle -Value $CallHandle
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name Type -Value $Type
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name Disposition -Value $Disposition
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name RemotePartyName -Value $RemotePartyName
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name RemotePartyNumber -Value $RemotePartyNumber
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name LocalPartyName -Value $LocalPartyName
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name LocalPartyNumber -Value $LocalPartyNumber
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name ConnectionName -Value $ConnectionName
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name ConnectionNumber -Value $ConnectionNumber
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name CallState -Value $CallState
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name Protocol -Value $Protocol
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name StartTime -Value $StartTime
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name Duration -Value $Duration
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name LineID -Value $LineID
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name CallSequence -Value $CallSequence
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name UIAppearanceIndex -Value $UIAppearanceIndex
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name ActiveCall -Value $UIAppearanceIndex #Temporary value
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name Ringing -Value $Ringing
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name Muted -Value $Muted
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name RTPPort -Value $RTPPort
  $ThisOutputCall | Add-Member -MemberType NoteProperty -Name RTCPPort -Value $RTCPPort

  if($MacAddress.Length -eq 12)
  {
    $ThisOutputCall | Add-Member -MemberType NoteProperty -Name MacAddress -Value $MacAddress
  }
  if($IPv4Address.Length -gt 0)
  {
    $ThisOutputCall | Add-Member -MemberType NoteProperty -Name IPv4Address -Value $IPv4Address
  }

  #Null any properties that weren't included.
  $NullProperties = New-Object System.Collections.ArrayList
  Foreach($Property in ($ThisOutputCall | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name))
  {
    $ThisValue = $ThisOutputCall.$Property
    $IsNull = $false

    if($null -eq $ThisValue)
    {
      $IsNull = $true
    }
    elseif($ThisValue.GetType().Name -eq "Int32")
    {
      if($ThisValue -le 0)
      {
        $IsNull = $true
      }
    }
    elseif($ThisValue.GetType().Name -eq "String")
    {
      if($ThisValue -eq "")
      {
        $IsNull = $true
      }
    }
    elseif($ThisValue.GetType().Name -eq "String[]")
    {
      #String array, smash into a string if it's only one long.
      if($ThisValue.Count -eq 0)
      {
        $IsNull = $true
      }
      elseif($ThisValue.Count -eq 1)
      {
        $ThisOutputCall.$Property = $ThisValue[0]
        Write-Debug "Property $Property was a single-value array. Smashed to string."
      }
    }
    else
    {
      Write-Debug "Value for property $Property was non-null: $ThisValue"
    }

    if($IsNull)
    {
      $ThisOutputCall.$Property = $null
      $null = $NullProperties.Add($Property)
    }
  }

  #Compute a start time based on duration and current time.
  if($null -eq $ThisOutputCall.StartTime -and $null -ne $ThisOutputCall.Duration -and $IsLog -ne $true)
  {
    $ThisOutputCall.StartTime = (Get-Date) - $ThisOutputCall.Duration
    $NullProperties = $NullProperties | Where-Object { $_ -ne "StartTime" }
  }
  elseif($null -eq $ThisOutputCall.Duration -and $null -ne $ThisOutputCall.StartTime -and $IsLog -ne $true)
  {
    #Calculate the duration. Drop milliseconds.
    $ThisDuration = (Get-Date) - (Get-Date $ThisOutputCall.StartTime)
    $ThisOutputCall.Duration = New-TimeSpan -Seconds ([Int]$ThisDuration.TotalSeconds)
    $NullProperties = $NullProperties | Where-Object { $_ -ne "Duration" }
  }

  if($null -ne $ThisOutputCall.CallHandle -and $ThisOutputCall.CallHandle -notmatch '^0x?[a-f0-9]{7,8}$' )
  {
    #If there's a callhandle that needs modification.
    $ThisOutputCall.CallHandle = ('0x{0}' -f $ThisOutputCall.CallHandle)
  }

  if($null -ne $ThisOutputCall.UIAppearanceIndex)
  {
    #If a UI Appearance Index is provided, compute if this is the active call.
    if($ThisOutputCall.UIAppearanceIndex -match '^\d+\*$')
    {
      $ActiveCall = $true
    }
    else
    {
      $ActiveCall = $false
    }
    $ThisOutputCall.UIAppearanceIndex = [Int]($ThisOutputCall.UIAppearanceIndex.Trim(' *'))
    $ThisOutputCall.ActiveCall = $ActiveCall
  }

  if($ThisOutputCall.CallState -eq 'CallHold')
  {
    #V1 REST API returns "CallHold" instead of "Hold," so we coerce it into the right format.
    $ThisOutputCall.CallState = 'Hold'
  }
  elseif($ThisOutputCall.CallState -eq 'Proceeding')
  {
    $ThisOutputCall.CallState = 'Connected'
  }

  #We standardize calls and call logs with the same names.
  if($ThisOutputCall.Type -eq 'Placed' -or $ThisOutputCall.Type -eq 'Out')
  {
    $ThisOutputCall.Type = 'Outgoing'
  }
  elseif($ThisOutputCall.Type -eq 'Received' -or $ThisOutputCall.Type -eq 'In')
  {
    $ThisOutputCall.Type = 'Incoming'
  }

  if($ExcludeNullProperties)
  {
    $ThisOutputCall = $ThisOutputCall | Select-Object -Property * -ExcludeProperty $NullProperties
  }

  Return $ThisOutputCall
}

Function Start-UcsSimultaneousJob
{
  <# Work in progress. Input a scriptblock with a placeholder for IP address. Use $Args as the placeholder for IP address. #>
  [CmdletBinding()]
  Param(
    [Parameter(Mandatory,HelpMessage = '127.0.0.1',ValueFromPipelineByPropertyName,ValueFromPipeline)][ValidatePattern('^([0-2]?[0-9]{1,2}\.){3}([0-2]?[0-9]{1,2})$')][String[]]$IPv4Address,
    [Parameter(Mandatory,HelpMessage = 'Use $Args in place of the IP address input.')][ScriptBlock]$ScriptBlock,
    [ValidateRange(1,100)][Int]$MaxJobs = 20,
    [ValidateRange(1,([int]::MaxValue))][Int]$JobChunkSize = 20,
    [ValidateRange(1,([int]::MaxValue))][Int]$TimeoutSeconds = 120 )

  $RandomizedIpV4AddressList = $IPv4Address | Get-Random -Count ($Ipv4Address.Count) #Randomize the order to prevent any particular job from being much slower than another.

  $AllJobs = New-Object System.Collections.ArrayList
  For($i = 0; $i -le ($IPv4Address.Count - 1); $i+=$JobChunkSize)
  {
    if($AllJobs.Count -ge $MaxJobs)
    {
      Write-Debug "Hit max job count. Waiting..."
      $WaitedJobs = Wait-Job -Id $AllJobs -Any
      $WaitedJobs = Get-Job -Id $AllJobs | Where-Object State -ne "Running"
      Write-Debug ("Got {0} done jobs." -f $WaitedJobs.Count)

      Foreach ($DoneJob in $WaitedJobs)
      {
        Write-Debug ("Got job {0}" -f $DoneJob.Name)
        $DoneJob | Receive-Job #Output the result.
        $null = $AllJobs.Remove($DoneJob.Id)
      }
    }

    $MaxIpIndex = $i+$JobChunkSize-1
    if($MaxIpIndex -gt ($IPv4Address.Count - 1))
    {
      $MaxIpIndex = $IPv4Address.Count - 1
    }

    $IPRange = $RandomizedIpV4AddressList[$i..$MaxIpIndex]
    $IPRange = @($IPRange) #Wrap the array to sidestep it getting unwrapped.
    Write-Debug ('Starting job beginning with IP {0}, total count {1} addresses.' -f $IPRange[0],$IPRange.Count)
    $ThisJob = Start-Job -ScriptBlock $ScriptBlock -ArgumentList $IPRange -Name ("{0}-{1}" -f $IPRange[0],[Guid]::NewGuid().ToString())
    $null = $AllJobs.Add($ThisJob.Id)
  }

  Write-Debug ("After completion of loops, {0} jobs are still pending at {1}" -f $AllJobs.Count,(Get-Date).ToShortTimeString())
  $FinalJobs = Wait-Job -Id $AllJobs -Timeout $TimeoutSeconds
  Write-Debug ("After waiting, {0} jobs are finished at {1}." -f $FinalJobs.Count,(Get-Date).ToShortTimeString())

  Foreach ($DoneJob in $FinalJobs)
  {
    $DoneJob | Receive-Job #Output the result.
    $null = $AllJobs.Remove($DoneJob.Id) #remove this ID from the remaining jobs.
  }

  Foreach($UnfinishedJob in $AllJobs)
  {
    Write-Warning ("Job {0} failed to complete within timeout period." -f $UnfinishedJob.Name)
    $null = $UnfinishedJob | Stop-Job
    $null = $UnfinishedJob | Remove-Job
  }

}

Function ConvertFrom-SecureString
{
  <#
    .SYNOPSIS
    Decrypt a SecureString for use in plaintext. This is inherently unsafe, but is the only way to send a plaintext string to the phone with a PsCredential. Returns only the password.
  #>

  Param([Parameter(Mandatory)][SecureString]$SecureString)
  Return [System.Runtime.InteropServices.marshal]::PtrToStringAuto([System.Runtime.InteropServices.marshal]::SecureStringToBSTR($SecureString))
}
Function ConvertFrom-PsCredential
{
  <#
    .SYNOPSIS
    Decrypt a PsCredential for use in plaintext. This is inherently unsafe, but is the only way to send a plaintext string to the phone with a PsCredential. Returns only the password.
  #>

  Param([Parameter(Mandatory)][PsCredential]$Credential)
  Return (ConvertFrom-SecureString -SecureString $Credential.Password)
}