Public/Get-mssOperationStatus.TempPoint.ps1

<#
.SYNOPSIS
Zeigt den Fortschritt und die geschätzte Restdauer für aktive Backup-, Restore- und AutoSeed-Operationen an.
 
.DESCRIPTION
Die Funktion überwacht aktive SQL Server-Operationen (Backup, Restore, AutoSeed) und berechnet
den Fortschritt sowie die geschätzte verbleibende Zeit. Sie kombiniert Informationen aus:
- Backup- und Restore-Fortschritt: sys.dm_exec_requests
- AutoSeed-Fortschritt: sys.dm_hadr_physical_seeding_stats
 
Die Funktion kann auf einer bestimmten Instanz ausgeführt werden und zeigt standardmäßig
alle aktiven Operationen an. Über Parameter kann nach Vorgangstyp (Backup, Restore, AutoSeed)
gefiltert werden.
 
Wenn kein SqlInstance-Parameter angegeben wird, wird standardmäßig der aktuelle
Computername ($env:COMPUTERNAME) verwendet. Diese Regel gilt für alle zukünftigen
Versionen.
 
.PARAMETER SqlInstance
Die Ziel-SQL Server-Instanz (Standard: aktueller Computername).
 
.PARAMETER SqlCredential
Alternative Anmeldeinformationen.
 
.PARAMETER OperationType
Filtert nach Vorgangstyp. Mögliche Werte: 'Backup', 'Restore', 'AutoSeed'.
Standardmäßig werden alle aktiven Operationen angezeigt.
 
.PARAMETER Continuous
Wenn gesetzt, wird die Ausgabe kontinuierlich aktualisiert (ähnlich wie 'watch').
Beenden mit Strg+C.
 
.PARAMETER RefreshSeconds
Intervall für die kontinuierliche Aktualisierung in Sekunden (Standard: 5). Nur in Verbindung mit -Continuous.
 
.PARAMETER EnableException
Schalter, um Ausnahmen durchzulassen (standardmäßig werden Fehler als Warnung protokolliert).
 
.EXAMPLE
# Alle aktiven Operationen auf der lokalen Instanz anzeigen
Get-mssOperationStatus
 
.EXAMPLE
# Nur aktive AutoSeed-Vorgänge auf einer entfernten Instanz
Get-mssOperationStatus -SqlInstance "SQL01" -OperationType AutoSeed
 
.EXAMPLE
# Kontinuierliche Aktualisierung alle 10 Sekunden
Get-mssOperationStatus -Continuous -RefreshSeconds 10
 
.NOTES
Erfordert dbatools und Invoke-mssLogging.
Die Berechnung der geschätzten Restdauer basiert auf dem bisherigen Fortschritt
und der vergangenen Zeit. Die Genauigkeit verbessert sich mit fortschreitender Operation.
#>

function Get-mssOperationStatus
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]$SqlInstance,
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$SqlCredential,
        [Parameter(Mandatory = $false)]
        [ValidateSet('Backup', 'Restore', 'AutoSeed')]
        [string]$OperationType,
        [Parameter(Mandatory = $false)]
        [switch]$Continuous,
        [Parameter(Mandatory = $false)]
        [int]$RefreshSeconds = 5,
        [Parameter(Mandatory = $false)]
        [switch]$EnableException
    )
    
    begin
    {
        $functionName = $MyInvocation.MyCommand.Name
        if (-not $PSBoundParameters.ContainsKey('SqlInstance') -or [string]::IsNullOrWhiteSpace($SqlInstance))
        {
            $SqlInstance = $env:COMPUTERNAME
        }
        if (-not (Get-Module -ListAvailable -Name dbatools))
        {
            throw "dbatools-Modul nicht gefunden."
        }
        Invoke-mssLogging -Message "Starte $functionName auf $SqlInstance" -FunctionName $functionName -Level "INFO"
    }
    
    process
    {
        do
        {
            try
            {
                # 1. Backup/Restore Operationen über sys.dm_exec_requests abrufen
                $backupRestoreQuery = @"
SELECT
    r.session_id,
    r.command,
    r.percent_complete,
    r.estimated_completion_time,
    r.start_time,
    DB_NAME(r.database_id) AS database_name,
    DATEADD(ms, r.estimated_completion_time, GETDATE()) AS expected_completion_time
FROM sys.dm_exec_requests r
WHERE r.command IN ('BACKUP DATABASE', 'RESTORE DATABASE', 'BACKUP LOG', 'RESTORE LOG')
    AND r.percent_complete > 0
"@

                
                $backupRestoreOps = Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Query $backupRestoreQuery -Database master -ErrorAction Stop
                
                # 2. AutoSeed Operationen über sys.dm_hadr_physical_seeding_stats abrufen
                # Diese DMV existiert ab SQL Server 2016
                $autoSeedQuery = @"
SELECT
    local_database_name AS database_name,
    role_desc,
    internal_state_desc,
    transfer_rate_bytes_per_second,
    transferred_size_bytes,
    total_size_bytes,
    start_time,
    estimated_completion_time_ms,
    CASE
        WHEN total_size_bytes > 0 THEN (transferred_size_bytes * 100.0 / total_size_bytes)
        ELSE 0
    END AS percent_complete
FROM sys.dm_hadr_physical_seeding_stats
WHERE internal_state_desc IN ('RUNNING', 'IN_PROGRESS')
"@

                
                $autoSeedOps = @()
                try
                {
                    $autoSeedOps = Invoke-DbaQuery -SqlInstance $SqlInstance -SqlCredential $SqlCredential -Query $autoSeedQuery -Database master -ErrorAction Stop
                }
                catch
                {
                    # DMV existiert möglicherweise nicht (z.B. SQL Server 2014 oder älter)
                    if ($_.Exception.Message -match 'Invalid object name')
                    {
                        Write-Verbose "sys.dm_hadr_physical_seeding_stats nicht verfügbar (SQL Server < 2016)"
                    }
                    else
                    {
                        throw
                    }
                }
                
                # Ergebnisse kombinieren und filtern
                $allOps = @()
                
                # Backup/Restore Operationen verarbeiten
                foreach ($op in $backupRestoreOps)
                {
                    $operationType = if ($op.command -match 'BACKUP') { 'Backup' }
                    else { 'Restore' }
                    if ($OperationType -and $OperationType -ne $operationType) { continue }
                    
                    $percentComplete = [math]::Round($op.percent_complete, 2)
                    $remainingSeconds = [math]::Round($op.estimated_completion_time / 1000, 0)
                    $remainingTimeFormatted = if ($remainingSeconds -gt 0)
                    {
                        Format-TimeSpan -Seconds $remainingSeconds
                    }
                    else { "Unbekannt" }
                    
                    $allOps += [PSCustomObject]@{
                        OperationType = $operationType
                        DatabaseName  = $op.database_name
                        SessionId      = $op.session_id
                        PercentComplete = $percentComplete
                        RemainingTime = $remainingTimeFormatted
                        RemainingSeconds = $remainingSeconds
                        ExpectedCompletion = if ($op.expected_completion_time) { $op.expected_completion_time } else { $null }
                        StartTime      = $op.start_time
                        TransferRate  = $null
                        TransferredSize = $null
                        TotalSize      = $null
                        Status          = $op.command
                    }
                }
                
                # AutoSeed Operationen verarbeiten
                foreach ($op in $autoSeedOps)
                {
                    if ($OperationType -and $OperationType -ne 'AutoSeed') { continue }
                    
                    $percentComplete = [math]::Round($op.percent_complete, 2)
                    $remainingSeconds = if ($op.estimated_completion_time_ms -gt 0)
                    {
                        [math]::Round($op.estimated_completion_time_ms / 1000, 0)
                    }
                    else { $null }
                    $remainingTimeFormatted = if ($remainingSeconds -gt 0)
                    {
                        Format-TimeSpan -Seconds $remainingSeconds
                    }
                    else { "Unbekannt" }
                    
                    $transferRateFormatted = if ($op.transfer_rate_bytes_per_second -gt 0)
                    {
                        Format-FileSize -Bytes $op.transfer_rate_bytes_per_second
                    }
                    else { "N/A" }
                    
                    $allOps += [PSCustomObject]@{
                        OperationType = 'AutoSeed'
                        DatabaseName  = $op.database_name
                        SessionId      = $null
                        PercentComplete = $percentComplete
                        RemainingTime = $remainingTimeFormatted
                        RemainingSeconds = $remainingSeconds
                        ExpectedCompletion = $null
                        StartTime      = $op.start_time
                        TransferRate  = $transferRateFormatted
                        TransferredSize = Format-FileSize -Bytes $op.transferred_size_bytes
                        TotalSize      = Format-FileSize -Bytes $op.total_size_bytes
                        Status          = $op.internal_state_desc
                    }
                }
                
                # Ausgabe
                if ($Continuous)
                {
                    Clear-Host
                    Write-Host "=== SQL Server Operation Status auf $SqlInstance ===" -ForegroundColor Cyan
                    Write-Host "Aktualisierung alle $RefreshSeconds Sekunden (Strg+C zum Beenden)" -ForegroundColor Gray
                    Write-Host ""
                }
                
                if ($allOps.Count -eq 0)
                {
                    $msg = "Keine aktiven $($OperationType -replace 'AutoSeed', 'AutoSeed-')Operationen gefunden."
                    Write-Host $msg -ForegroundColor Yellow
                    Invoke-mssLogging -Message $msg -FunctionName $functionName -Level "INFO"
                }
                else
                {
                    # Formatierte Ausgabe als Tabelle
                    $displayProps = @(
                        @{ Name = 'Type'; Expression = { $_.OperationType } },
                        @{ Name = 'Database'; Expression = { $_.DatabaseName } },
                        @{ Name = 'Status'; Expression = { $_.Status } },
                        @{ Name = 'Progress'; Expression = { "$($_.PercentComplete)%" } },
                        @{ Name = 'Remaining'; Expression = { $_.RemainingTime } }
                    )
                    
                    # Zusätzliche Spalten für AutoSeed
                    if ($allOps | Where-Object { $_.OperationType -eq 'AutoSeed' })
                    {
                        $displayProps += @{ Name = 'Rate'; Expression = { $_.TransferRate } }
                        $displayProps += @{ Name = 'Transferred'; Expression = { $_.TransferredSize } }
                    }
                    
                    $allOps | Select-Object -Property ($displayProps | ForEach-Object { $_.Name }) | Format-Table -AutoSize
                }
                
                # Kontinuierliche Ausführung
                if ($Continuous)
                {
                    Start-Sleep -Seconds $RefreshSeconds
                }
            }
            catch
            {
                $errMsg = "Fehler beim Abrufen der Operationen: $($_.Exception.Message)"
                Invoke-mssLogging -Message $errMsg -FunctionName $functionName -Level "ERROR"
                if ($EnableException) { throw }
                Write-Error $errMsg
                if ($Continuous) { Start-Sleep -Seconds $RefreshSeconds }
            }
        }
        while ($Continuous)
    }
    
    end
    {
        Invoke-mssLogging -Message "$functionName abgeschlossen." -FunctionName $functionName -Level "INFO"
    }
}

# Hilfsfunktionen für Zeit- und Größenformatierung
function Format-TimeSpan
{
    param ([int]$Seconds)
    $ts = [TimeSpan]::FromSeconds($Seconds)
    if ($ts.TotalHours -ge 1)
    {
        return "{0}h {1}m {2}s" -f [math]::Floor($ts.TotalHours), $ts.Minutes, $ts.Seconds
    }
    elseif ($ts.TotalMinutes -ge 1)
    {
        return "{0}m {1}s" -f $ts.Minutes, $ts.Seconds
    }
    else
    {
        return "{0}s" -f $ts.Seconds
    }
}

function Format-FileSize
{
    param ([long]$Bytes)
    if ($Bytes -ge 1TB)
    {
        return "{0:N2} TB" -f ($Bytes / 1TB)
    }
    elseif ($Bytes -ge 1GB)
    {
        return "{0:N2} GB" -f ($Bytes / 1GB)
    }
    elseif ($Bytes -ge 1MB)
    {
        return "{0:N2} MB" -f ($Bytes / 1MB)
    }
    elseif ($Bytes -ge 1KB)
    {
        return "{0:N2} KB" -f ($Bytes / 1KB)
    }
    else
    {
        return "$Bytes B"
    }
}