Public/AzStackHci.MemoryDumpSettings.ps1

# ///////////////////////////////////////////////////////////////////
# Set-AzStackHciMemoryDumpSettings Main function
# Used to set the registry settings required to obtain a kernel memory dump
# ///////////////////////////////////////////////////////////////////
Function Set-AzStackHciMemoryDumpSettings {
    <#
    .SYNOPSIS
 
    Sets the registry settings required to obtain a kernel memory dump
 
    .DESCRIPTION
 
    Sets the fourteen recommended registry settings required for the system to generate a kernel memory dump.
    Enabling memory dumps on the nodes with large memory size, the size of the dedicated dump file is
    calculated based on the system memory present in the node.
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType([void])]
    param (
        # Path to drive letter for DedicatedDumpFile
        [Parameter(Mandatory=$true,Position=1)]
        [ValidateScript({Test-Path $_})]
        [String]
        [ValidatePattern('^[C-Zc-z]:\\?$')]$DedicatedDumpFileDriveLetter, # Drive letter for the dedicated dump file, must be a valid drive letter (C: through Z:)

        # Optional switch, used to minimize the page file size if required to save space
        [Parameter(Mandatory=$false,Position=2)]
        [Switch]
        $ConfigureMinimumPageFile,

        # Optional switch, used to ignore disk space check for the dedicated dump file
        [Parameter(Mandatory=$false,Position=3)]
        [Switch]
        $IgnoreDiskSpaceCheck,

        [Parameter(Mandatory=$false, HelpMessage="Optional switch to prevent console output from the function.")]
        [switch]$NoOutput
    )

    begin {
        # Requires administrator permissions to set memory dump settings in the registry
        if (-not (Test-Elevation)) { throw "This script must be run as an Administrator." }
    }

    process {
        # Handle -NoOutput: suppress all console output
        if ($NoOutput.IsPresent) {
            $script:SilentMode = $true
            $VerbosePreference = 'SilentlyContinue'
            $DebugPreference = 'SilentlyContinue'
        }

        # if the drive letter is only 2 characters (C:), add a backslash to the end (C:\)
        if($DedicatedDumpFileDriveLetter.Length -eq 2){
            $DedicatedDumpFileDriveLetter = $($DedicatedDumpFileDriveLetter + "\")
        }

        # Call function to Set the Size of the DedicateDumpFile based on node memory size
        [uint32]$script:DedicatedDumpFileSize = SetDedicatedDumpFileSize

        # Call function to Set the Path of the DedicateDumpFile and validate there is sufficient space
        if($IgnoreDiskSpaceCheck.IsPresent){
            # Call function to Set the Path of the DedicateDumpFile and ignore disk space check
            [string]$script:DedicatedDumpFilePath = SetDedicatedDumpFilePath $DedicatedDumpFileDriveLetter -IgnoreDiskSpaceCheck
        } else {
            # Call function to Set the Path of the DedicateDumpFile and validate there is sufficient space
            [string]$script:DedicatedDumpFilePath = SetDedicatedDumpFilePath $DedicatedDumpFileDriveLetter
        }

        if($ConfigureMinimumPageFile.IsPresent){
            # Call function to Set Page File to minimum 4GB settings
            Set-AzStackHciPageFileMinimumSettings -PageFileFileDriveLetter $DedicatedDumpFileDriveLetter
        }

        # Call function to Get and Save current page file settings
        Get-AzStackHciMemoryDumpSettings

        $HKLMCrashControl = "HKLM:\System\CurrentControlSet\Control\CrashControl"

        # ShouldProcess gate: prompt user before modifying registry settings
        if (-not $PSCmdlet.ShouldProcess("CrashControl registry ($HKLMCrashControl)", "Set kernel memory dump registry values")) {
            return
        }

        # Capture original registry values for rollback in case of partial failure
        $registryNames = @('AutoReboot','DisableEmoticon','CrashDumpEnabled','FilterPages','NMICrashDump',
            'DedicatedDumpFile','DumpFileSize','IgnorePagefileSize','AlwaysKeepMemoryDump','LogEvent',
            'DumpFile','MinidumpsCount','Overwrite','MinidumpDir')
        $originalValues = @{}
        foreach ($name in $registryNames) {
            try {
                $originalValues[$name] = (Get-ItemProperty -Path $HKLMCrashControl -Name $name -ErrorAction Stop).$name
            } catch {
                # Setting does not exist yet — mark for removal on rollback
                $originalValues[$name] = $null
            }
        }

        try {
            Set-ItemProperty -Path $HKLMCrashControl -Name AutoReboot -Type DWord -Value 1 -ErrorAction Stop # Automatic reboot
            Set-ItemProperty -Path $HKLMCrashControl -Name DisableEmoticon -Type DWord -Value 1 -ErrorAction Stop # Disable emoticons (display bug check error information on console)
            Set-ItemProperty -Path $HKLMCrashControl -Name CrashDumpEnabled -Type DWord -Value 2 -ErrorAction Stop # Kernel memory dump
            Set-ItemProperty -Path $HKLMCrashControl -Name FilterPages -Type DWord -Value 1 -ErrorAction Stop # Filter pages, used for active memory dump, but still set it for kernel memory dump
            Set-ItemProperty -Path $HKLMCrashControl -Name NMICrashDump -Type DWord -Value 1 -ErrorAction Stop # Support NMI crashes
            Set-ItemProperty -Path $HKLMCrashControl -Name DedicatedDumpFile -Type String -Value $DedicatedDumpFilePath -ErrorAction Stop # Dedicated Dump File Path
            Set-ItemProperty -Path $HKLMCrashControl -Name DumpFileSize -Type DWord -Value $DedicatedDumpFileSize -ErrorAction Stop # Dedicated Dump File size
            Set-ItemProperty -Path $HKLMCrashControl -Name IgnorePagefileSize -Type DWord -Value 1 -ErrorAction Stop # Required for large memory systems
            Set-ItemProperty -Path $HKLMCrashControl -Name AlwaysKeepMemoryDump -Type DWord -Value 1 -ErrorAction Stop # Prevents automatic deletion of memory dump files
            Set-ItemProperty -Path $HKLMCrashControl -Name LogEvent -Type DWord -Value 1 -ErrorAction Stop # Log event 1001 in System log
            # Memory dump file, should be the same drive as the dedicated dump file
            Set-ItemProperty -Path $HKLMCrashControl -Name DumpFile -Type ExpandString -Value "$($DedicatedDumpFileDriveLetter)memory.dmp" -ErrorAction Stop
            # Minidump settings
            Set-ItemProperty -Path $HKLMCrashControl -Name MinidumpsCount -Type DWord -Value 1 -ErrorAction Stop
            Set-ItemProperty -Path $HKLMCrashControl -Name Overwrite -Type DWord -Value 1 -ErrorAction Stop
            if(-not(test-path "C:\Windows\Minidump")) { New-Item "C:\Windows\Minidump" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null }
            Set-ItemProperty -Path $HKLMCrashControl -Name MinidumpDir -Type ExpandString -Value "C:\Windows\Minidump\" -ErrorAction Stop
        } catch {
            # Rollback: restore original values to avoid leaving the system in a partially configured state
            Write-Verbose "Registry write failed, rolling back to original values. Error: $($_.Exception.Message)" -Verbose
            foreach ($name in $registryNames) {
                try {
                    if ($null -eq $originalValues[$name]) {
                        # Setting did not exist before — remove it if it was created
                        Remove-ItemProperty -Path $HKLMCrashControl -Name $name -Force -ErrorAction SilentlyContinue
                    } else {
                        Set-ItemProperty -Path $HKLMCrashControl -Name $name -Value $originalValues[$name] -ErrorAction SilentlyContinue
                    }
                } catch {
                    Write-Warning "Rollback: Failed to restore '$name'. Manual intervention may be required."
                }
            }
            Throw "Failed to set CrashControl registry settings (rolled back). Error: $($_.Exception.Message)"
        }

        Write-Verbose "System memory dump settings have been configured for a Kernel Memory Dump to be written to '$($DedicatedDumpFileDriveLetter)memory.dmp'" -Verbose
        Write-Verbose "Dedicated Dump File configured to '$DedicatedDumpFilePath', with a size of $DedicatedDumpFileSize MiB ($([math]::round($DedicatedDumpFileSize*1MB/1Gb)) GiB).`n`n" -Verbose
        Write-Verbose "A system restart is required for the Memory Dump changes to take effect." -Verbose

    } # End of process block

    end {
        if ($NoOutput.IsPresent) { $script:SilentMode = $false }
        Write-Debug "Completed Set-AzStackHciMemoryDumpSettings function"
    }

} # End of Set-AzStackHciMemoryDumpSettings


# ///////////////////////////////////////////////////////////////////
# Restore-AzStackHciMemoryDumpSettings function
# Used to restore or roll back crash control registry settings
# ///////////////////////////////////////////////////////////////////
Function Restore-AzStackHciMemoryDumpSettings
{
    <#
    .SYNOPSIS
 
    Restores registry values for crash control / memory dump settings
 
    .DESCRIPTION
 
    Restores the fourteen recommended registry settings from an earlier time, using a backup file as input.
    Requires administrator permissions to restore memory dump settings in the registry
    #>


    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType([void])]
    param (
        # File path to backup file
        [Parameter(Mandatory=$true,Position=0)]
        [ValidateScript({Test-Path $_})]
        [String]$MemoryDumpSettingsFilePath, # File used to restore settings from, must be a valid path

        [Parameter(Mandatory=$false, HelpMessage="Optional switch to prevent console output from the function.")]
        [switch]$NoOutput
    )

    begin {
        # Requires administrator permissions to set memory dump settings in the registry
        if (-not (Test-Elevation)) { throw "This script must be run as an Administrator." }
    }

    process {
        # Handle -NoOutput: suppress all console output
        if ($NoOutput.IsPresent) {
            $script:SilentMode = $true
            $VerbosePreference = 'SilentlyContinue'
            $DebugPreference = 'SilentlyContinue'
        }

        # Read the memory dump settings from the backup file into an array
        # Resolve the path first to prevent directory traversal (e.g. '..') within the allowed prefix
        try {
            $ResolvedBackupPath = (Resolve-Path -Path $MemoryDumpSettingsFilePath -ErrorAction Stop).Path
        } catch {
            throw "Error: Unable to resolve backup file path '$MemoryDumpSettingsFilePath'. File may not exist. Error: $($_.Exception.Message)"
        }
        if($ResolvedBackupPath -like "C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_*"){
            Write-Verbose "Reading Memory Dump settings from backup file: $ResolvedBackupPath" -Verbose
        } else {
            Write-Verbose "Memory Dump settings backup file path must resolve to 'C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_*'" -Verbose
            throw "Error: Memory Dump settings backup file path must resolve to 'C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_*'. Resolved path: '$ResolvedBackupPath'"
        }
        # Use the resolved path for all subsequent operations
        $MemoryDumpSettingsFilePath = $ResolvedBackupPath
        [string[]]$RestoreMemoryDumpSettings = Get-Content $MemoryDumpSettingsFilePath -ErrorAction Stop

        $HKLMCrashControl = "HKLM:\System\CurrentControlSet\Control\CrashControl"

        # Date time the Memory Dump settings were saved should be the first line in the file
        try {
            [datetime]$DateTimeMemoryDumpSettings = $RestoreMemoryDumpSettings[0]    
        }
        catch {
            Write-Verbose "Failed to get the date and time the Memory Dump settings were saved. Error: $($_.Exception.Message)" -Verbose
            throw "Error: Failed to get the date and time the Memory Dump settings were saved. Error: $($_.Exception.Message)"
        }
        
        # Confirm with user before restoring the memory dump settings
        Write-Verbose "Restoring Memory Dump settings from $MemoryDumpSettingsFilePath, saved on $DateTimeMemoryDumpSettings" -Verbose
        if (-not $PSCmdlet.ShouldProcess("CrashControl registry ($HKLMCrashControl)", "Restore memory dump settings from backup saved on $DateTimeMemoryDumpSettings")) {
            return
        }
        
        # Restore the memory dump settings, the first six lines (0-5) are the date and time, and the next 14 lines are the settings
        # i = 6 is the seventh line in the file, which is the first setting
        for($i=6; $i -lt ($RestoreMemoryDumpSettings.Count -2); $i++)
        {
            $Setting = $RestoreMemoryDumpSettings[$i]
            # Debugging output, when running with -Debug
            Write-Debug "Backup file input: $Setting"
            # Split the setting into the name and value
            $SettingName = $Setting.Split(" ",[System.StringSplitOptions]::RemoveEmptyEntries)[0].Trim()
            $SettingValue = $Setting.Split(" ",[System.StringSplitOptions]::RemoveEmptyEntries)[1].Trim()
            # Check if the setting value is a comment, as these are not set in the registry
            if($SettingValue -eq "//Setting"){ 
                $SettingValue = $Setting.Split("//",[System.StringSplitOptions]::RemoveEmptyEntries)[1].Trim()
            }
            # Check if the setting was not configured, so will need deleting to restore the settings
            if($SettingValue -eq "Setting not configured") {
                # Check if the setting is present in the registry
                Remove-Variable TestValue -ErrorAction SilentlyContinue
                try {
                    Get-ItemProperty -Path $HKLMCrashControl -Name $SettingName -ErrorAction SilentlyContinue -ErrorVariable TestValue | Out-Null
                } catch {
                    Write-Verbose "Setting '$SettingName' not present in the registry." -Verbose
                }
                # Execute the setting removal if it was not configured and is present in the registry
                if(-not($TestValue)) { 
                    # Value exists, remove the setting if it was not configured in the backup file
                    Write-Verbose "Removing setting '$SettingName' from the registry." -Verbose
                    Remove-ItemProperty -Path $HKLMCrashControl -Name $SettingName -Force -ErrorAction Stop | Out-Null
                } else {
                    Write-Verbose "Info: Expected setting '$SettingName' to be configured in the registry, but is not." -Verbose
                }
            # Setting is configured still, restore the setting value from the backup file
            } else {
                # Get the current setting in the registry, to get the type of the value
                Remove-Variable TestValue -ErrorAction SilentlyContinue
                try {
                    $RegValue = Get-ItemProperty -Path $HKLMCrashControl -Name $SettingName -ErrorAction SilentlyContinue -ErrorVariable TestValue
                } catch {
                    Write-Error "Setting '$SettingName' not present in the registry." -Verbose
                }
                if(-not($TestValue)) { 
                    # Get the type of the registry value
                    $RegValueType = (($RegValue | Get-Member | Where-Object{$_.Name -eq $SettingName}).Definition -split " ")[0]
                    if($RegValueType -eq "int") {
                        # Convert the registry value type to DWord
                        $RegValueType = "DWord"
                    } elseif($RegValueType -eq "string") {
                        # Convert the registry value type to ExpandString
                        $RegValueType = "ExpandString"
                    }
                    # Restore the registry value to match the backup file setting
                    try {
                        Write-Verbose "Restoring setting '$SettingName' to value '$SettingValue', type = '$RegValueType'." -Verbose
                        Set-ItemProperty -Path $HKLMCrashControl -Name $SettingName -Type $RegValueType -Value $SettingValue -ErrorAction Stop
                    }
                    catch {
                        Write-Error "Failed to restore setting '$SettingName' to value '$SettingValue'. Error: $($_.Exception.Message)"
                    }
                # Error, the setting is not present in the registry!
                } else {
                    throw "Expected setting '$SettingName' to be configured in the registry, but is not."
                }
            }

        } # End of for loop
        Write-Verbose "Memory dump settings have been restored from backup file." -Verbose
        Write-Verbose "A system restart is required for the Memory Dump changes to take effect." -Verbose

    } # End of process block

    end {
        if ($NoOutput.IsPresent) { $script:SilentMode = $false }
        Write-Debug "Completed Restore-AzStackHciMemoryDumpSettings function"
    }

} # End of Restore-AzStackHciMemoryDumpSettings


# ///////////////////////////////////////////////////////////////////
# Get-AzStackHciMemoryDumpSettings function
# Used to get the current memory dump settings on the system
# ///////////////////////////////////////////////////////////////////
Function Get-AzStackHciMemoryDumpSettings {
    <#
    .SYNOPSIS
 
    Gets current crash dump settings
 
    .DESCRIPTION
 
    Queries the registry for current crash dump settings
 
    #>


    [CmdletBinding()]
    [OutputType([void])]
    param (
        [switch]$NoOutput
    )

    begin {
        # Requires administrator permissions to get crash dump settings and write to log file to disk
        if (-not (Test-Elevation)) { throw "This script must be run as an Administrator." }
    }

    process {
        # Handle -NoOutput: suppress all console output
        if ($NoOutput.IsPresent) {
            $script:SilentMode = $true
            $VerbosePreference = 'SilentlyContinue'
            $DebugPreference = 'SilentlyContinue'
        }

        # Call function, to Set the Size of the DedicateDumpFile based on node memory size
        [uint32]$script:DedicatedDumpFileSize = SetDedicatedDumpFileSize

        $HKLMCrashControl = "HKLM:\System\CurrentControlSet\Control\CrashControl"
        $CrashControlSettings = Get-Item -Path $HKLMCrashControl -ErrorAction Stop | Get-ItemProperty -ErrorAction Stop

        $script:CurrentDumpFile = $CrashControlSettings.DumpFile
        
        $HKLMCrashControl = "HKLM:\System\CurrentControlSet\Control\CrashControl"
        $CrashControlProperties = "AutoReboot","DisableEmoticon","LogEvent","CrashDumpEnabled","FilterPages","NMICrashDump","DedicatedDumpFile","DumpFileSize","IgnorePagefileSize","AlwaysKeepMemoryDump","DumpFile","MinidumpsCount","Overwrite","MinidumpDir"

        $script:CurrentSettings = @{} # Create a hashtable to store the current settings properties
        foreach($PropertyName in $CrashControlProperties)
        {
            # Get-ItemPropertyValue throws even with -ErrorAction SilentlyContinue
            $CurrentSetting = Get-ItemProperty $HKLMCrashControl -Name $PropertyName -ErrorAction SilentlyContinue
            if ($CurrentSetting)
            {
                # Add description
                if($PropertyName -eq "CrashDumpEnabled"){
                    $CurrentCrashDumpMode = switch ($CurrentSetting.CrashDumpEnabled) {
                        1 { if ($CurrentSetting.FilterPages) { "CrashDumpEnabled = 1 (Active Memory Dump)" } else { "CrashDumpEnabled = 1 (Complete Memory Dump)" } }
                        2 {"CrashDumpEnabled = 2 (Kernel Memory Dump)"}
                        3 {"CrashDumpEnabled = 3 (Small Memory Dump)"}
                        7 {"CrashDumpEnabled = 7 (Automatic Memory Dump)"}
                        default {"Unknown"}
                    }
                }
                # Add actual configured value
                $CurrentSettings.$PropertyName = $CurrentSetting.$PropertyName
            } else {
                $CurrentSettings.$PropertyName = "//Setting not configured"
            }
        }
        # Output the current settings to the console
        Write-Verbose "Current Memory Dump settings:`n" -Verbose
        $CurrentSettings | Out-String | Write-Verbose -Verbose
        $DateFormatted = Get-Date -f "yyyyMMdd"
        if(-not(Test-Path "C:\ProgramData\AzStackHci.DiagnosticSettings\"))
        {
            New-Item "C:\ProgramData\AzStackHci.DiagnosticSettings\" -ItemType Directory -ErrorAction SilentlyContinue | Out-Null
        }
        "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss")" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_$DateFormatted.txt") -ErrorAction Stop
        "Backup of Crash Dump registry settings on $env:COMPUTERNAME - (current configuration = $CurrentCrashDumpMode)`n" | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
        $CurrentSettings | Out-File $("C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_$DateFormatted.txt") -Append -ErrorAction Stop
        Write-Verbose "Current Memory Dump settings exported to 'C:\ProgramData\AzStackHci.DiagnosticSettings\MemoryDump_Settings_$DateFormatted.txt'" -Verbose

    } # End of process block

    end {
        if ($NoOutput.IsPresent) { $script:SilentMode = $false }
        Write-Debug "Completed Get-AzStackHciMemoryDumpSettings function"
    }

} # End of Get-AzStackHciMemoryDumpSettings function