Autonance.psm1

<#
    .SYNOPSIS
    Autonance DSL maintenance container.
 
    .DESCRIPTION
    The Maintenance container is part of the Autonance domain-specific language
    (DSL) and is used to define a maintenance container block. The maintenance
    container block is always the topmost block and contains all sub containers
    and maintenance tasks.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function Maintenance
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')]
    param
    (
        # Name of the maintenance.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $Name,

        # Script block containing the maintenance tasks.
        [Parameter(Mandatory = $true, Position = 1)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock,

        # Optionally parameters to use for all maintenance tasks.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = $null,

        # Hide autonance header.
        [Parameter(Mandatory = $false)]
        [switch]
        $NoHeader,

        # Hide autonance output.
        [Parameter(Mandatory = $false)]
        [switch]
        $NoOutput
    )

    try
    {
        # Ensure that the Maintenance block is not nested
        if ($Script:AutonanceBlock)
        {
            throw 'Maintenance container is not the topmost block'
        }

        # Initialize context variables
        $Script:AutonanceBlock  = $true
        $Script:AutonanceLevel  = 0
        $Script:AutonanceSilent = $NoOutput.IsPresent

        # Headline with module info
        if (!$NoHeader.IsPresent)
        {
            Write-Autonance -Message (Get-Module -Name 'Autonance' | ForEach-Object { "{0} Version {1}`n{2}" -f $_.Name, $_.Version, $_.Copyright }) -Type 'Info'
        }

        # Create the maintenance container object
        $containerSplat = @{
            Type            = 'Maintenance'
            Name            = $Name
            Credential      = $Credential
            ScriptBlock     = $ScriptBlock
        }
        $container = New-AutonanceContainer @containerSplat

        # Invoke the root maintenance container
        Invoke-AutonanceContainer -Container $container

        # End blank lines
        Write-Autonance -Message "`n" -Type 'Info'
    }
    finally
    {
        # Reset context variable
        $Script:AutonanceBlock     = $false
        $Script:AutonanceTimestamp = $null
    }
}

<#
    .SYNOPSIS
    Autonance DSL container to group maintenance tasks.
 
    .DESCRIPTION
    The TaskGroup container is part of the Autonance domain-specific language
    (DSL) and is used to group maintenance tasks. Optionally, the tasks within
    the group can be repeated in a loop. The loop can have multiple stop options
    or will run infinite as long as no exception occurs.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function TaskGroup
{
    [CmdletBinding(DefaultParameterSetName = 'Simple')]
    param
    (
        # Task group name.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $Name,

        # Script block containing the grouped tasks.
        [Parameter(Mandatory = $true, Position = 1)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock,

        # Optionally parameters to use for all maintenance tasks.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential = $null,

        # Option to repeat all tasks in the group.
        [Parameter(Mandatory = $false, ParameterSetName = 'Repeat')]
        [switch]
        $Repeat,

        # Number of times to repeat the group. Use 0 for an infinite loop.
        [Parameter(Mandatory = $false, ParameterSetName = 'Repeat')]
        [System.Int32]
        $RepeatCount = 0,

        # Script block to control the repeat loop. Return $true to continue with
        # the loop. Return $false to stop the loop and continue with the next
        # maintenance task.
        [Parameter(Mandatory = $false, ParameterSetName = 'Repeat')]
        [System.Management.Automation.ScriptBlock]
        $RepeatCondition = $null,

        # Option to show a user prompt after each loop, to inquire, if the loop
        # should continue or stop.
        [Parameter(Mandatory = $false, ParameterSetName = 'Repeat')]
        [switch]
        $RepeatInquire
    )

    # Create and return the task group container object
    $containerSplat = @{
        Type            = 'TaskGroup'
        Name            = $Name
        Credential      = $Credential
        ScriptBlock     = $ScriptBlock
        Repeat          = $Repeat.IsPresent
        RepeatCount     = $RepeatCount
        RepeatCondition = $RepeatCondition
        RepeatInquire   = $RepeatInquire.IsPresent
    }
    New-AutonanceContainer @containerSplat
}

<#
    .SYNOPSIS
    Get the registered Autonance extensions.
 
    .DESCRIPTION
    This command wil return all registered autonance extensions in the current
    PowerShell session.
 
    .EXAMPLE
    PS C:\> Get-AutonanceExtension
    Returns all registered autonance extensions.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function Get-AutonanceExtension
{
    [CmdletBinding()]
    param
    (
        # Name of the extension to return
        [Parameter(Mandatory = $false)]
        [System.String]
        $Name
    )

    foreach ($key in $Script:AutonanceExtension.Keys)
    {
        if ($null -eq $Name -or $key -like $Name)
        {
            $Script:AutonanceExtension[$key]
        }
    }
}

<#
    .SYNOPSIS
    Register an Autonance extension.
 
    .DESCRIPTION
    This function will register a new Autonance extension, by creating a global
    function with the specified extension name. The function can be called like
    all other DSL tasks in the Maintenance block.
    The script block can contain a parameter block, to specify the parameters
    provided to the cmdlet. If a parameter $Credential is used, the credentials
    will automatically be passed to the sub task, if specified. The function
    Write-AutonanceMessage can be used to return status messages. The Autonance
    module will take care of the formatting.
 
    .EXAMPLE
    PS C:\>
    Register-AutonanceExtension -Name 'WsusReport' -ScriptBlock {
        [CmdletBinding()]
        param
        (
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,
 
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,
        )
 
        if ($null -eq $Credential)
        {
            Invoke-Command -ComputerName $ComputerName -ScriptBlock { wuauclt.exe /ReportNow }
        }
        else
        {
            Invoke-Command -ComputerName $ComputerName -Credential $Credential -ScriptBlock { wuauclt.exe /ReportNow }
        }
    }
 
    Register an Autonance extension to invoke the report now command for WSUS.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function Register-AutonanceExtension
{
    [CmdletBinding()]
    param
    (
        # Extension function name.
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        # Script block to execute for the extension.
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock
    )

    # Register the Autonance extension in the current scope
    $Script:AutonanceExtension[$Name] = [PSCustomObject] [Ordered] @{
        PSTypeName  = 'Autonance.Extension'
        Name        = $Name
        ScriptBlock = $ScriptBlock
    }

    # Create the helper script block which will be invoked for the extension. It
    # return the extension as a Autonance task item.
    $extensionScriptBlock = {

        $name      = (Get-PSCallStack)[0].InvocationInfo.MyCommand.Name
        $extension = Get-AutonanceExtension -Name $name

        # Create and return the task object without using New-AutonanceTask
        # function, because this will be executed outside of the module scope
        # and there is the helper function New-AutonanceTask not available.
        [PSCustomObject] [Ordered] @{
            PSTypeName  = 'Autonance.Task'
            Type        = $extension.Name
            Name        = ''
            Credential  = $Credential
            ScriptBlock = $extension.ScriptBlock
            Arguments   = $PSBoundParameters
        }
    }

    # Concatenate the original parameters and the helper script block
    $extensionParameter   = [String] $ScriptBlock.Ast.ParamBlock
    $extensionBody        = [String] $extensionScriptBlock
    $extensionScriptBlock = [ScriptBlock]::Create($extensionParameter + $extensionBody)

    # Register the global function
    Set-Item -Path "Function:\Global:$Name" -Value $extensionScriptBlock -Force | Out-Null
}

<#
    .SYNOPSIS
    Unregister an Autonance extension.
 
    .DESCRIPTION
    This function removes a registered Autonance extension from the current
    session.
 
    .EXAMPLE
    PS C:\> Unregister-AutonanceExtension -Name 'WsusReport'
    Unregister the Autonance extension calles WsusReport.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function Unregister-AutonanceExtension
{
    [CmdletBinding()]
    param
    (
        # Extension function name.
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name
    )

    if ($Script:AutonanceExtension.ContainsKey($Name))
    {
        # Remove the module extension
        $Script:AutonanceExtension.Remove($Name)

        # Remove the global function
        Remove-Item -Path "Function:\$Name" -Force
    }
}

<#
    .SYNOPSIS
    Write an Autonance task message.
 
    .DESCRIPTION
    This function must be used in a Autonance extension task to show the current
    status messages in a nice formatted output. The Autonance module will take
    care about the indent and message color.
 
    .EXAMPLE
    PS C:\>
    Register-AutonanceExtension -Name 'ShowMessage' -ScriptBlock {
        Write-AutonanceMessage -Message 'Hello, World!'
    }
 
    Uses the Write-AutonanceMessage function to show a nice formatted output
    message within a custom Autonance task.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function Write-AutonanceMessage
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $Message
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'Write-AutonanceMessage function not encapsulated in a Autonance task'
    }

    Write-Autonance -Message $Message
}

<#
    .SYNOPSIS
    Autonance DSL task to get a user confirmation.
 
    .DESCRIPTION
    The ConfirmTask task is part of the Autonance domain-specific language
    (DSL). The task uses the $Host.UI.PromptForChoice() built-in method
    to display a host specific confirm prompt.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function ConfirmTask
{
    [CmdletBinding()]
    param
    (
        # Message title for the confirm box.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $Caption,

        # Message body for the confirm box.
        [Parameter(Mandatory = $true, Position = 1)]
        [System.String]
        $Query
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'ConfirmTask task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'ConfirmTask' -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # Message title for the confirm box.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $Caption,

            # Message body for the confirm box.
            [Parameter(Mandatory = $true, Position = 1)]
            [System.String]
            $Query
        )

        # Prepare the choices
        $choices = New-Object -TypeName 'Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]'
        $choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Yes', 'Continue the maintenance'))
        $choices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&No', 'Stop the maintenance'))

        # Query the desired choice from the user
        do
        {
            $result = $Host.UI.PromptForChoice($Caption, $Query, $choices, -1)
        }
        while ($result -eq -1)

        # Check the result and quit the execution, if necessary
        if ($result -ne 0)
        {
            throw "User has canceled the maintenance!"
        }
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to invoke a local script block.
 
    .DESCRIPTION
    The LocalScript task is part of the Autonance domain-specific language
    (DSL). The task will invoke the script block on the local computer. The
    script can use some of the built-in PowerShell functions to return objects
    or control the maintenance:
    - Throw an terminating error to stop the whole maintenance script
    - Show status information with Write-Autonance
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function LocalScript
{
    [CmdletBinding()]
    param
    (
        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # The script block to invoke.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'LocalScript task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'LocalScript' -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # The script block to invoke.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.Management.Automation.ScriptBlock]
            $ScriptBlock
        )

        $ErrorActionPreference = 'Stop'

        if ($null -eq $Credential)
        {
            Write-Autonance -Message 'Invoke the local script block now...'

            & $ScriptBlock
        }
        else
        {
            try
            {
                Write-Autonance -Message "Push the impersonation context as $($Credential.UserName)"

                Push-ImpersonationContext -Credential $Credential -ErrorAction Stop

                Write-Autonance -Message 'Invoke the local script block now...'

                & $ScriptBlock
            }
            finally
            {
                Write-Autonance -Message 'Pop the impersonation context'

                Pop-ImpersonationContext -ErrorAction SilentlyContinue
            }
        }
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to invoke a remote script block.
 
    .DESCRIPTION
    The RemoteScript task is part of the Autonance domain-specific language
    (DSL). The task will invoke the script block on the specified Windows
    computer using WinRM. A user account can be specified with the Credential
    parameter. The script can use some of the built-in PowerShell functions to
    return objects or control the maintenance:
    - Throw an terminating error to stop the whole maintenance script
    - Show status information with Write-Autonance
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function RemoteScript
{
    [CmdletBinding()]
    param
    (
        # This task restarts the specified Windows computer.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $ComputerName,

        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # The script block to invoke.
        [Parameter(Mandatory = $true, Position = 1)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'RemoteScript task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'RemoteScript' -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # This task restarts the specified Windows computer.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,

            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # The script block to invoke.
            [Parameter(Mandatory = $true, Position = 1)]
            [System.Management.Automation.ScriptBlock]
            $ScriptBlock
        )

        try
        {
            $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType WinRM -ErrorAction Stop

            Write-Autonance -Message "Invoke the remote script block now..."

            Invoke-Command -Session $session -ScriptBlock $ScriptBlock -ErrorAction Stop
        }
        catch
        {
            throw $_
        }
        finally
        {
            Remove-AutonanceSession -Session $session
        }
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to wait for the specified amount of time.
 
    .DESCRIPTION
    The SleepTask task is part of the Autonance domain-specific language (DSL).
    The task uses the Start-Sleep built-in command to wait for the specified
    amount of time.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function SleepTask
{
    [CmdletBinding()]
    param
    (
        # Duration in seconds to wait.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.Int32]
        $Second
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'SleepTask task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'SleepTask' -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # Duration in seconds to wait.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.Int32]
            $Second
        )

        Write-Autonance -Message "Wait for $Second second(s)"

        # Now wait
        Start-Sleep -Seconds $Second
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to failover an SQL Server Availability Group.
 
    .DESCRIPTION
    Use this task to failover the specified SQL Server Availability Group to the
    specified computer and instance. It will use the SQLPS module on the remote
    system.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function SqlServerAvailabilityGroupFailover
{
    [CmdletBinding()]
    param
    (
        # This is the target Windows computer for the planned failover.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $ComputerName,

        # Target SQL instance for the planned planned.
        [Parameter(Mandatory = $true, Position = 1)]
        [System.String]
        $SqlInstance,

        # The availability group name to perform a planned manual failover.
        [Parameter(Mandatory = $true, Position = 2)]
        [System.String]
        $AvailabilityGroup,

        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # Specifies the number of retries between availability group state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Count = 60,

        # Specifies the interval between availability group state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Delay = 2
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'SqlServerAvailabilityGroupFailover task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'SqlServerAvailabilityGroupFailover' -Name "$ComputerName $SqlInstance $AvailabilityGroup" -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # This is the target Windows computer for the planned failover.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,

            # Target SQL instance for the planned planned.
            [Parameter(Mandatory = $true, Position = 1)]
            [System.String]
            $SqlInstance,

            # The availability group name to perform a planned manual failover.
            [Parameter(Mandatory = $true, Position = 2)]
            [System.String]
            $AvailabilityGroup,

            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # Specifies the number of retries between availability group state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Count = 60,

            # Specifies the interval between availability group state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Delay = 2
        )

        $SqlInstancePath = $SqlInstance

        # Default MSSQLSERVER instance, only specified as server name
        if (-not $SqlInstance.Contains('\'))
        {
            $SqlInstancePath = "$SqlInstance\DEFAULT"
        }

        try
        {
            ## Part 1 - Connect to the SQL Server and load the SQLPS module

            $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType WinRM -ErrorAction Stop

            # Load the SQL PowerShell module but suppress warnings because of
            # uncommon cmdlet verbs.
            Invoke-Command -Session $session -ScriptBlock { Import-Module -Name 'SQLPS' -WarningAction 'SilentlyContinue' } -ErrorAction Stop


            ## Part 2 - Check the current role and state

            $replicas = Invoke-Command -Session $session -ScriptBlock { Get-ChildItem -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup\AvailabilityReplicas" | Select-Object * } -ErrorAction Stop
            $replica  = $replicas.Where({$_.Name -eq $SqlInstance})[0]


            ## Part 3 - Planned manual failover

            if ($replica.Role -ne 'Primary')
            {
                ## Part 3a - Check replicate state

                Write-Autonance -Message "Replica role is $($replica.Role.ToString().ToLower())"
                Write-Autonance -Message "Replica state is $($replica.RollupRecoveryState.ToString().ToLower()) and $($replica.RollupSynchronizationState.ToString().ToLower())"

                if ($replica.RollupRecoveryState.ToString() -ne 'Online' -or $replica.RollupSynchronizationState.ToString() -ne 'Synchronized')
                {
                    throw 'Replicate is not ready for planned manual failover!'
                }


                ## Part 3b - Invoke failover

                Write-Autonance -Message "Failover $AvailabilityGroup to $SqlInstance ..."

                Invoke-Command -Session $session -ScriptBlock { Switch-SqlAvailabilityGroup -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup" } -ErrorAction Stop

                Wait-AutonanceTask -Activity "$SqlInstance replication is restoring ..." -Count $Count -Delay $Delay -Condition {

                    $replicas = Invoke-Command -Session $session -ScriptBlock { Get-ChildItem -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup\AvailabilityReplicas" | ForEach-Object { $_.Refresh(); $_ } } -ErrorAction Stop

                    $condition = $true

                    # Check all replica states
                    foreach ($replica in $replicas)
                    {
                        # Test for primary replica
                        if ($replica.Name -eq $SqlInstance)
                        {
                            $condition = $condition -and $replica.Role -eq 'Primary'
                            $condition = $condition -and $replica.RollupRecoveryState -eq 'Online'
                        }

                        # Test for any replica
                        $condition = $condition -and $replica.RollupSynchronizationState -eq 'Synchronized'
                    }

                    $condition
                }
            }


            ## Part 4 - Verify

            $replicas = Invoke-Command -Session $session -ScriptBlock { Get-ChildItem -Path "SQLSERVER:\Sql\$using:SqlInstancePath\AvailabilityGroups\$using:AvailabilityGroup\AvailabilityReplicas" } -ErrorAction Stop
            $replica  = $replicas.Where({$_.Name -eq $SqlInstance})[0]

            Write-Autonance -Message "Replica role is $($replica.Role.ToString().ToLower())"
            Write-Autonance -Message "Replica state is $($replica.RollupRecoveryState.ToString().ToLower()) and $($replica.RollupSynchronizationState.ToString().ToLower())"

            if ($replica.Role -ne 'Primary')
            {
                throw 'Replica role is not primary'
            }

            if ($replica.RollupRecoveryState -ne 'Online')
            {
                throw 'Replica recovery state is not online'
            }

            if ($replica.RollupSynchronizationState -ne 'Synchronized')
            {
                throw 'Replica synchronization state is not synchronized'
            }
        }
        catch
        {
            throw $_
        }
        finally
        {
            Remove-AutonanceSession -Session $session

            # Ensure, that the next task has a short delay
            Start-Sleep -Seconds 3
        }
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to restart a Windows computer.
 
    .DESCRIPTION
    The WindowsComputerRestart task is part of the Autonance domain-specific
    language (DSL). The task will restart the specified Windows computer using
    WinRM. A user account can be specified with the Credential parameter.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function WindowsComputerRestart
{
    [CmdletBinding()]
    param
    (
        # This task restarts the specified Windows computer.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $ComputerName,

        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # Specifies the number of retries between computer state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Count = 120,

        # Specifies the interval between computer state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Delay = 5
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'WindowsComputerRestart task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'WindowsComputerRestart' -Name $ComputerName -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # This task restarts the specified Windows computer.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,

            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # Specifies the number of retries between computer state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Count = 120,

            # Specifies the interval between computer state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Delay = 5
        )

        try
        {
            ## Part 1 - Before Reboot

            $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop

            # Get the operating system object
            $operatingSystem = Get-CimInstance -CimSession $session -ClassName 'Win32_OperatingSystem' -ErrorAction Stop

            # To verify the reboot, store the last boot up time
            $oldBootUpTime = $operatingSystem.LastBootUpTime

            Write-Autonance -Message "Last boot up time is $oldBootUpTime"


            ## Part 2 - Execute Reboot

            Write-Autonance -Message "Restart computer $ComputerName now ..."

            # Now, reboot the system
            $result = $operatingSystem | Invoke-CimMethod -Name 'Reboot' -ErrorAction Stop

            # Check method error code
            if ($result.ReturnValue -ne 0)
            {
                $errorMessage = Get-AutonanceErrorMessage -Component 'Win32OperatingSystem_Wbem' -ErrorId $result.ReturnValue

                throw "Failed to restart $ComputerName with error code $($result.ReturnValue): $errorMessage"
            }

            # Close old session (or try it...)
            Remove-AutonanceSession -Session $session -Silent

            # Reset variables
            $session         = $null
            $operatingSystem = $null

            # Wait until the computer has restarted is running
            Wait-AutonanceTask -Activity "$ComputerName is restarting..." -Count $Count -Delay $Delay -Condition {

                # Prepare credentials for remote connection
                $credentialSplat = @{}
                if ($null -ne $Credential)
                {
                    $credentialSplat['Credential'] = $Credential
                }

                # Test the connection and try to get the operating system object
                $innerOperatingSystem = Invoke-Command -ComputerName $ComputerName @credentialSplat -ScriptBlock { Get-CimInstance -ClassName 'Win32_OperatingSystem' } -WarningAction SilentlyContinue -ErrorAction SilentlyContinue

                # Return boolean value if the condition has passed
                $null -ne $innerOperatingSystem -and $oldBootUpTime -lt $innerOperatingSystem.LastBootUpTime
            }


            ## Part 3 - Verify Reboot

            $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop

            # Get the operating system object
            $operatingSystem = Get-CimInstance -CimSession $session -ClassName 'Win32_OperatingSystem' -ErrorAction Stop

            # To verify the reboot, store the new boot up time
            $newBootUpTime = $operatingSystem.LastBootUpTime

            Write-Autonance -Message "New boot up time is $newBootUpTime"

            # Verify if the reboot was successful
            if ($oldBootUpTime -eq $newBootUpTime)
            {
                throw "Failed to restart computer $ComputerName!"
            }
        }
        catch
        {
            throw $_
        }
        finally
        {
            Remove-AutonanceSession -Session $session

            # Ensure, that the next task has a short delay
            Start-Sleep -Seconds 5
        }
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to shutdown a Windows computer.
 
    .DESCRIPTION
    The WindowsComputerShutdown task is part of the Autonance domain-specific
    language (DSL). The task will shutdown the specified Windows computer. A
    user account can be specified with the Credential parameter.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function WindowsComputerShutdown
{
    [CmdletBinding()]
    param
    (
        # This task stops the specified Windows computer.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $ComputerName,

        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # Specifies the number of retries between computer state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Count = 720,

        # Specifies the interval between computer state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Delay = 5
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'WindowsComputerShutdown task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'WindowsComputerShutdown' -Name $ComputerName -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # This task stops the specified Windows computer.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,

            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # Specifies the number of retries between computer state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Count = 720,

            # Specifies the interval between computer state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Delay = 5
        )

        ## Part 1 - Before Shutdown

        $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop

        # Get the operating system object
        $operatingSystem = Get-CimInstance -CimSession $session -ClassName 'Win32_OperatingSystem' -ErrorAction Stop

        Write-Autonance -Message "Last boot up time is $($operatingSystem.LastBootUpTime)"


        ## Part 2 - Execute Shutdown

        Write-Autonance -Message "Shutdown computer $ComputerName now ..."

        # Now, reboot the system
        $result = $operatingSystem | Invoke-CimMethod -Name 'Shutdown' -ErrorAction Stop

        # Check method error code
        if ($result.ReturnValue -ne 0)
        {
            $errorMessage = Get-AutonanceErrorMessage -Component 'Win32OperatingSystem_Wbem' -ErrorId $result.ReturnValue

            throw "Failed to shutdown $ComputerName with error code $($result.ReturnValue): $errorMessage"
        }

        # Close old session (or try it...)
        Remove-AutonanceSession -Session $session -Silent


        ## Part 3 - Wait for Shutdown

        # Wait until the computer has is disconnected
        Wait-AutonanceTask -Activity "Wait for computer $ComputerName disconnect..." -Count $Count -Delay $Delay -Condition {

            try
            {
                # Prepare credentials for remote connection
                $credentialSplat = @{}
                if ($null -ne $Credential)
                {
                    $credentialSplat['Credential'] = $Credential
                }

                # Test the connection and try to get the computer name
                Invoke-Command -ComputerName $ComputerName @credentialSplat -ScriptBlock { $Env:ComputerName } -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null

                return $false
            }
            catch
            {
                return $true
            }
        }
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to wait for a Windows computer.
 
    .DESCRIPTION
    The WindowsComputerWait task is part of the Autonance domain-specific
    language (DSL). The task will wait until the specified Windows computer is
    reachable using WinRM. A user account can be specified with the Credential
    parameter.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function WindowsComputerWait
{
    [CmdletBinding()]
    param
    (
        # This task waits for the specified Windows computer.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $ComputerName,

        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # Specifies the number of retries between computer state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Count = 720,

        # Specifies the interval between computer state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Delay = 5
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'WindowsComputerWait task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'WindowsComputerWait' -Name $ComputerName -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # This task waits for the specified Windows computer.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,

            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # Specifies the number of retries between computer state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Count = 720,

            # Specifies the interval between computer state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Delay = 5
        )

        Write-Autonance -Message "Wait for computer $ComputerName..."

        # Wait until the computer is reachable
        Wait-AutonanceTask -Activity "Wait for computer $ComputerName..." -Count $Count -Delay $Delay -Condition {

            try
            {
                # Prepare credentials for remote connection
                $credentialSplat = @{}
                if ($null -ne $Credential)
                {
                    $credentialSplat['Credential'] = $Credential
                }

                # Test the connection and try to get the computer name
                Invoke-Command -ComputerName $ComputerName @credentialSplat -ScriptBlock { $Env:ComputerName } -WarningAction SilentlyContinue -ErrorAction Stop | Out-Null

                return $true
            }
            catch
            {
                return $false
            }
        }
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to configure a Windows service.
 
    .DESCRIPTION
    The WindowsServiceConfig task is part of the Autonance domain-specific
    language (DSL). The task will configure the specified Windows service on a
    remote computer by using CIM to connect to the remote computer. A user
    account can be specified with the Credential parameter.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function WindowsServiceConfig
{
    [CmdletBinding()]
    param
    (
        # This task configures a Windows service on the specified computer.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $ComputerName,

        # Specifies the service name for the service to be configured.
        [Parameter(Mandatory = $true, Position = 1)]
        [System.String]
        $ServiceName,

        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # If specified, the target startup type will be set.
        [Parameter(Mandatory = $false)]
        [ValidateSet('Automatic', 'Manual', 'Disabled')]
        [System.String]
        $StartupType
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'WindowsServiceConfig task not encapsulated in a Maintenance container'
    }

    # Define a nice task name
    $name = "$ComputerName\$ServiceName"
    if ($PSBoundParameters.ContainsKey('StartupType'))
    {
        $name += ", StartupType=$StartupType"
    }

    New-AutonanceTask -Type 'WindowsServiceConfig' -Name $name -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # This task configures a Windows service on the specified computer.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,

            # Specifies the service name for the service to be configured.
            [Parameter(Mandatory = $true, Position = 1)]
            [System.String]
            $ServiceName,

            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # If specified, the target startup type will be set.
            [Parameter(Mandatory = $false)]
            [ValidateSet('Automatic', 'Manual', 'Disabled')]
            [System.String]
            $StartupType
        )

        try
        {
            $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop

            # Get service and throw an exception, if the service does not exist
            $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop
            if ($null -eq $service)
            {
                throw "$ServiceName service does not exist!"
            }

            if ($PSBoundParameters.ContainsKey('StartupType'))
            {
                # Do nothing, if the services startup type is correct
                if ($service.StartMode.Replace('Auto', 'Automatic') -eq $StartupType)
                {
                    Write-Autonance -Message "$ServiceName service startup type is already set to $StartupType"
                }
                else
                {
                    Write-Autonance -Message "$ServiceName service startup type is $($service.StartMode.Replace('Auto', 'Automatic'))"
                    Write-Autonance -Message "Set $ServiceName service startup type to $StartupType"

                    # Reconfigure service
                    $result = $service | Invoke-CimMethod -Name 'ChangeStartMode' -Arguments @{ StartMode = $StartupType } -ErrorAction Stop

                    # Check method error code
                    if ($result.ReturnValue -ne 0)
                    {
                        throw "Failed to set $ServiceName service startup type with error code $($result.ReturnValue)!"
                    }

                    # Check if the service startup type is correct
                    $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop
                    if ($service.StartMode.Replace('Auto', 'Automatic') -ne $StartupType)
                    {
                        throw "Failed to set $ServiceName service startup type, current is $($service.StartMode.Replace('Auto', 'Automatic'))!"
                    }

                    Write-Autonance -Message "$ServiceName service startup type changed successfully"
                }
            }
        }
        catch
        {
            throw $_
        }
        finally
        {
            Remove-AutonanceSession -Session $session
        }
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to start a Windows service.
 
    .DESCRIPTION
    The WindowsServiceStart task is part of the Autonance domain-specific
    language (DSL). The task will start the specified Windows service on a
    remote computer by using CIM to connect to the remote computer. A user
    account can be specified with the Credential parameter.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function WindowsServiceStart
{
    [CmdletBinding()]
    param
    (
        # This task starts a Windows service on the specified computer.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $ComputerName,

        # Specifies the service name for the service to be started.
        [Parameter(Mandatory = $true, Position = 1)]
        [System.String]
        $ServiceName,

        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # Specifies the number of retries between service state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Count = 30,

        # Specifies the interval between service state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Delay = 2
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'WindowsServiceStart task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'WindowsServiceStart' -Name "$ComputerName\$ServiceName" -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # This task starts a Windows service on the specified computer.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,

            # Specifies the service name for the service to be started.
            [Parameter(Mandatory = $true, Position = 1)]
            [System.String]
            $ServiceName,

            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # Specifies the number of retries between service state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Count = 30,

            # Specifies the interval between service state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Delay = 2
        )

        try
        {
            $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop

            # Get service and throw an exception, if the service does not exist
            $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop
            if ($null -eq $service)
            {
                throw "$ServiceName service does not exist!"
            }

            # Do nothing, if the services is already running
            if ($service.State -eq 'Running')
            {
                Write-Autonance -Message "$ServiceName service is already running"
            }
            else
            {
                Write-Autonance -Message "$ServiceName service is not running"
                Write-Autonance -Message "Start $ServiceName service now"

                # Start service
                $result = $service | Invoke-CimMethod -Name 'StartService' -ErrorAction Stop

                # Check method error code
                if ($result.ReturnValue -ne 0)
                {
                    $errorMessage = Get-AutonanceErrorMessage -Component 'Win32Service_StartService' -ErrorId $result.ReturnValue

                    throw "Failed to start $ServiceName service with error code $($result.ReturnValue): $errorMessage!"
                }

                # Wait until the services is running
                Wait-AutonanceTask -Activity "$ServiceName service is starting..." -Count $Count -Delay $Delay -Condition {
                    $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop
                    $service.State -ne 'Start Pending'
                }

                # Check if the service is now running
                $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop
                if ($service.State -ne 'Running')
                {
                    throw "Failed to start $ServiceName service, current state is $($service.State)!"
                }

                Write-Autonance -Message "$ServiceName service started successfully"
            }
        }
        catch
        {
            throw $_
        }
        finally
        {
            Remove-AutonanceSession -Session $session
        }
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to stop a Windows service.
 
    .DESCRIPTION
    The WindowsServiceStop task is part of the Autonance domain-specific
    language (DSL). The task will stop the specified Windows service on a
    remote computer by using CIM to connect to the remote computer. A user
    account can be specified with the Credential parameter.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function WindowsServiceStop
{
    [CmdletBinding()]
    param
    (
        # This task stops a Windows service on the specified computer.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $ComputerName,

        # Specifies the service name for the service to be stopped.
        [Parameter(Mandatory = $true, Position = 1)]
        [System.String]
        $ServiceName,

        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # Specifies the number of retries between service state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Count = 30,

        # Specifies the interval between service state tests.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $Delay = 2
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'WindowsServiceStop task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'WindowsServiceStop' -Name "$ComputerName\$ServiceName" -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # This task stops a Windows service on the specified computer.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,

            # Specifies the service name for the service to be stopped.
            [Parameter(Mandatory = $true, Position = 1)]
            [System.String]
            $ServiceName,

            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # Specifies the number of retries between service state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Count = 30,

            # Specifies the interval between service state tests.
            [Parameter(Mandatory = $false)]
            [System.Int32]
            $Delay = 2
        )

        try
        {
            $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType CIM -ErrorAction Stop

            # Get service and throw an exception, if the service does not exist
            $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop
            if ($null -eq $service)
            {
                throw "$ServiceName service does not exist!"
            }

            # Do nothing, if the services is already running
            if ($service.State -eq 'Stopped')
            {
                Write-Autonance -Message "$ServiceName service is already stopped"
            }
            else
            {
                Write-Autonance -Message "$ServiceName service is not stopped"
                Write-Autonance -Message "Stop $ServiceName service now"

                # Stop service
                $result = $service | Invoke-CimMethod -Name 'StopService' -ErrorAction Stop

                # Check method error code
                if ($result.ReturnValue -ne 0)
                {
                    $errorMessage = Get-AutonanceErrorMessage -Component 'Win32Service_StopService' -ErrorId $result.ReturnValue

                    throw "Failed to stop $ServiceName service with error code $($result.ReturnValue): $errorMessage!"
                }

                # Wait until the services is stopped
                Wait-AutonanceTask -Activity "$ServiceName service is stopping..." -Count $Count -Delay $Delay -Condition {
                    $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop
                    $service.State -ne 'Stop Pending'
                }

                # Check if the service is now stopped
                $service = Get-CimInstance -CimSession $session -ClassName 'Win32_Service' -Filter "Name = '$ServiceName'" -ErrorAction Stop
                if ($service.State -ne 'Stopped')
                {
                    throw "Failed to stop $ServiceName service, current state is $($service.State)!"
                }

                Write-Autonance -Message "$ServiceName service stopped successfully"
            }
        }
        catch
        {
            throw $_
        }
        finally
        {
            Remove-AutonanceSession -Session $session
        }
    }
}

<#
    .SYNOPSIS
    Autonance DSL task to install Windows updates.
 
    .DESCRIPTION
    The WindowsUpdateInstall task is part of the Autonance domain-specific
    language (DSL). The task will install all pending Windows updates on the
    target Windows computer by using WinRM and the 'Microsoft.Update.Session'
    COM object. A user account can be specified with the Credential parameter.
 
    .NOTES
    Author : Claudio Spizzi
    License : MIT License
 
    .LINK
    https://github.com/claudiospizzi/Autonance
#>


function WindowsUpdateInstall
{
    [CmdletBinding()]
    param
    (
        # This task restarts the specified Windows computer.
        [Parameter(Mandatory = $true, Position = 0)]
        [System.String]
        $ComputerName,

        # Specifies a user account that has permission to perform the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # If specified, all available updates will be installed without a query.
        [Parameter(Mandatory = $false)]
        [switch]
        $All
    )

    if (!$Script:AutonanceBlock)
    {
        throw 'WindowsUpdateInstall task not encapsulated in a Maintenance container'
    }

    New-AutonanceTask -Type 'WindowsUpdateInstall' -Name $ComputerName -Credential $Credential -Arguments $PSBoundParameters -ScriptBlock {

        [CmdletBinding()]
        param
        (
            # This task restarts the specified Windows computer.
            [Parameter(Mandatory = $true, Position = 0)]
            [System.String]
            $ComputerName,

            # Specifies a user account that has permission to perform the task.
            [Parameter(Mandatory = $false)]
            [System.Management.Automation.PSCredential]
            [System.Management.Automation.Credential()]
            $Credential,

            # If specified, all available updates will be installed without a query.
            [Parameter(Mandatory = $false)]
            [switch]
            $All
        )

        try
        {
            $guid   = New-Guid | Select-Object -ExpandProperty 'Guid'
            $script = Get-Content -Path "$Script:ModulePath\Scripts\WindowsUpdate.ps1"

            $session = New-AutonanceSession -ComputerName $ComputerName -Credential $Credential -SessionType WinRM -ErrorAction Stop


            ## Part 1: Search for pending updates

            Write-Autonance -Message 'Search for pending updates ...'

            $pendingUpdates = Invoke-Command -Session $session -ErrorAction Stop -ScriptBlock {

                $updateSession  = New-Object -ComObject 'Microsoft.Update.Session'
                $updateSearcher = $updateSession.CreateUpdateSearcher()
                $searchResult   = $updateSearcher.Search("IsInstalled=0 and Type='Software'")

                foreach ($update in $searchResult.Updates)
                {
                    [PSCustomObject] @{
                        KBArticle = 'KB' + $update.KBArticleIDs[0]
                        Identity  = $update.Identity.UpdateID
                        Title     = $update.Title
                    }
                }
            }

            if ($pendingUpdates.Count -eq 0)
            {
                Write-Autonance -Message 'No pending updates found'
                return
            }

            Write-Autonance -Message ("{0} pending update(s) found" -f $pendingUpdates.Count)


            ## Part 2: Select updates

            if ($All.IsPresent)
            {
                Write-Autonance -Message 'All pending update(s) were preselected to install'

                $selectedUpdates = $pendingUpdates
            }
            else
            {
                Write-Autonance -Message 'Query the user for update(s) to install'

                $readHostMultipleChoiceSelection = @{
                    Caption      = 'Choose Updates'
                    Message      = 'Please select the updates to install from the following list.'
                    ChoiceObject = $pendingUpdates
                    ChoiceLabel  = $pendingUpdates.Title
                }
                $selectedUpdates = @(Read-HostMultipleChoiceSelection @readHostMultipleChoiceSelection)

                if ($selectedUpdates.Count -eq 0)
                {
                    Write-Autonance -Message 'No updates selected by the user'
                    return
                }

                Write-Autonance -Message ("{0} pending update(s) were selected to install" -f $selectedUpdates.Count)
            }


            ## Part 3: Install the updates with a one-time scheduled task

            Write-Autonance -Message 'Invoke a remote scheduled task to install the update(s)'

            # Create and start the scheduled task
            Invoke-Command -Session $session -ErrorAction Stop -ScriptBlock {

                $using:script | Set-Content -Path "C:\Windows\Temp\WindowsUpdate-$using:guid.ps1" -Encoding UTF8

                $updateList = $using:selectedUpdates.Identity -join ','

                try
                {
                    # Use the new scheduled tasks cmdlets
                    $newScheduledTask = @{
                        Action    = New-ScheduledTaskAction -Execute 'powershell.exe' -Argument "-NoProfile -ExecutionPolicy Bypass -File `"C:\Windows\Temp\WindowsUpdate-$using:guid.ps1`" -Id `"$using:guid`" -Update `"$updateList`"" -ErrorAction Stop
                        Trigger   = New-ScheduledTaskTrigger -Once -At (Get-Date).AddSeconds(-1) -ErrorAction Stop
                        Principal = New-ScheduledTaskPrincipal -UserId 'SYSTEM' -RunLevel Highest -ErrorAction Stop
                        Settings  = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -ErrorAction Stop
                    }
                    New-ScheduledTask @newScheduledTask -ErrorAction Stop | Register-ScheduledTask -TaskName "WindowsUpdate-$using:guid" -ErrorAction Stop | Start-ScheduledTask -ErrorAction Stop
                }
                catch
                {
                    # Craete a temporary batch file
                    "powershell.exe -NoProfile -ExecutionPolicy Bypass -File `"C:\Windows\Temp\WindowsUpdate-$using:guid.ps1`" -Id `"$using:guid`" -Update `"$updateList`"" | Out-File "C:\Windows\Temp\WindowsUpdate-$using:guid.cmd" -Encoding Ascii

                    # The scheduled tasks cmdlets are missing, use schtasks.exe
                    (SCHTASKS.EXE /CREATE /RU "NT Authority\System" /SC ONCE /ST 23:59 /TN "WindowsUpdate-$using:guid" /TR "`"C:\Windows\Temp\WindowsUpdate-$using:guid.cmd`"" /RL HIGHEST /F) | Out-Null
                    (SCHTASKS.EXE /RUN /TN "WindowsUpdate-$using:guid") | Out-Null
                }
            }

            # Wait for every step until it is completed
            foreach ($step in @('Search', 'Download', 'Install'))
            {
                $status = Invoke-Command -Session $session -ErrorAction Stop -ScriptBlock {

                    $step = $using:step
                    $path = "C:\Windows\Temp\WindowsUpdate-$using:guid.xml"

                    do
                    {
                        Start-Sleep -Seconds 1

                        if (Test-Path -Path $path)
                        {
                            $status = Import-Clixml -Path $path
                        }
                    }
                    while ($null -eq $status -or $status.$step.Status -eq $false)

                    Write-Output $status.$step
                }

                Write-Autonance -Message $status.Message

                if (-not $status.Result)
                {
                    throw $status.Message
                }
            }
        }
        catch
        {
            throw $_
        }
        finally
        {
            # Try to cleanup the scheduled task and the script file
            if ($null -ne $session)
            {
                Invoke-Command -Session $session -ErrorAction SilentlyContinue -ScriptBlock {

                    Unregister-ScheduledTask -TaskName "WindowsUpdate-$using:guid" -Confirm:$false -ErrorAction SilentlyContinue
                    Remove-Item -Path "C:\Windows\Temp\WindowsUpdate-$using:guid.cmd" -Force -ErrorAction SilentlyContinue
                    Remove-Item -Path "C:\Windows\Temp\WindowsUpdate-$using:guid.ps1" -Force -ErrorAction SilentlyContinue
                    Remove-Item -Path "C:\Windows\Temp\WindowsUpdate-$using:guid.xml" -Force -ErrorAction SilentlyContinue
                }
            }

            Remove-AutonanceSession -Session $session

            # Ensure, that the next task has a short delay
            Start-Sleep -Seconds 3
        }
    }
}


function Get-AutonanceErrorMessage
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Component,

        [Parameter(Mandatory = $false)]
        [System.String]
        $ErrorId
    )

    $values = Import-PowerShellDataFile -Path "$Script:ModulePath\Strings\$Component.psd1"

    $errorMessage = $values[$result.ReturnValue]
    if ([String]::IsNullOrEmpty($errorMessage))
    {
        $errorMessage = 'Unknown'
    }

    Write-Output $errorMessage
}


function Initialize-ImpersonationContext
{
    [CmdletBinding()]
    param ()

    # Add Win32 native API methods to call to LogonUser()
    if (-not ([System.Management.Automation.PSTypeName]'Win32.AdvApi32').Type)
    {
        Add-Type -Namespace 'Win32' -Name 'AdvApi32' -MemberDefinition '
            [DllImport("advapi32.dll", SetLastError = true)]
            public static extern bool LogonUser(string lpszUserName, string lpszDomain, string lpszPassword, int dwLogonType, int dwLogonProvider, out IntPtr phToken);
        '

    }

    # Add Win32 native API methods to call to CloseHandle()
    if (-not ([System.Management.Automation.PSTypeName]'Win32.Kernel32').Type)
    {
        Add-Type -Namespace 'Win32' -Name 'Kernel32' -MemberDefinition '
            [DllImport("kernel32.dll", SetLastError = true)]
            public static extern bool CloseHandle(IntPtr handle);
        '

    }

    # Define enumeration for the logon type
    if (-not ([System.Management.Automation.PSTypeName]'Win33.Logon32Type').Type)
    {
        Add-Type -TypeDefinition '
            namespace Win32
            {
                public enum Logon32Type
                {
                    Interactive = 2,
                    Network = 3,
                    Batch = 4,
                    Service = 5,
                    Unlock = 7,
                    NetworkClearText = 8,
                    NewCredentials = 9
                }
            }
        '

    }

    # Define enumeration for the logon provider
    if (-not ([System.Management.Automation.PSTypeName]'Win33.Logon32Type').Type)
    {
        Add-Type -TypeDefinition '
            namespace Win32
            {
                public enum Logon32Provider
                {
                    Default = 0,
                    WinNT40 = 2,
                    WinNT50 = 3
                }
            }
        '

    }

    # Global variable to hold the impersonation context
    if ($null -eq $Script:ImpersonationContext)
    {
        $Script:ImpersonationContext = New-Object -TypeName 'System.Collections.Generic.Stack[System.Security.Principal.WindowsImpersonationContext]'
    }
}


function Invoke-AutonanceContainer
{
    [CmdletBinding()]
    param
    (
        # Autonance container to execute.
        [Parameter(Mandatory = $true)]
        [PSTypeName('Autonance.Container')]
        $Container
    )

    $repeat      = $Container.Repeat
    $repeatCount = 1

    do
    {
        # Block info
        if ($repeat)
        {
            Write-Autonance '' -Type Info
            Write-Autonance "$($Container.Type) $($Container.Name) (Repeat: $repeatCount)" -Type Container
        }
        else
        {
            Write-Autonance '' -Type Info
            Write-Autonance "$($Container.Type) $($Container.Name)" -Type Container
        }

        # It's a container, so increment the level
        $Script:AutonanceLevel++

        # Get all items to execute
        $items = & $Container.ScriptBlock

        # Invoke all
        foreach ($item in $items)
        {
            # Inherit the credentials to all sub items
            if ($null -eq $item.Credential -and $null -ne $Container.Credential)
            {
                $item.Credential = $Container.Credential
            }

            if ($item.PSTypeNames -contains 'Autonance.Task')
            {
                Invoke-AutonanceTask -Task $item
            }
            elseif ($item.PSTypeNames -contains 'Autonance.Container')
            {
                Invoke-AutonanceContainer -Container $item
            }
            else
            {
                Write-Warning "Unexpected Autonance task or container object: [$($item.GetType().FullName)] $item"
            }
        }

        # Check repeat
        if ($repeat)
        {
            if ($Container.RepeatCount -ne 0)
            {
                if ($repeatCount -ge $Container.RepeatCount)
                {
                    $repeat = $false
                }
            }

            if ($null -ne $Container.RepeatCondition)
            {
                $repeatCondition = & $Container.RepeatCondition

                if (!$repeatCondition)
                {
                    $repeat = $false
                }
            }

            if ($Container.RepeatInquire)
            {
                # Prepare the choices
                $repeatInquireChoices = New-Object -TypeName 'Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]'
                $repeatInquireChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Repeat', 'Repeat all child tasks'))
                $repeatInquireChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Continue', 'Continue with next task'))

                # Query the desired choice from the user
                do
                {
                    $repeatInquireResult = $Host.UI.PromptForChoice('Repeat', "Do you want to repeat $($Container.Type) $($Container.Name)?", $repeatInquireChoices, -1)
                }
                while ($repeatInquireResult -eq -1)

                # Check the result and quit the execution, if necessary
                if ($repeatInquireResult -eq 1)
                {
                    $repeat = $false
                }
            }
        }

        # Increment repeat count
        $repeatCount++

        # Container has finished, decrement the level
        $Script:AutonanceLevel--
    }
    while ($repeat)
}


function Invoke-AutonanceTask
{
    [CmdletBinding()]
    param
    (
        # Autonance task to execute.
        [Parameter(Mandatory = $true)]
        [PSTypeName('Autonance.Task')]
        $Task
    )

    $retry      = $false
    $retryCount = 0

    do
    {
        # Block info
        if ($retryCount -eq 0)
        {
            Write-Autonance '' -Type Info
            Write-Autonance "$($Task.Type) $($Task.Name)" -Type Task
        }
        else
        {
            Write-Autonance '' -Type Info
            Write-Autonance "$($Task.Type) $($Task.Name) (Retry: $retryCount)" -Type Task
        }

        try
        {
            $taskArguments   = $task.Arguments
            $taskScriptBlock = $task.ScriptBlock

            # If the task supports custom credentials and the credentials were
            # not explicit specified, set them with the parent task credentials.
            if ($taskScriptBlock.Ast.ParamBlock.Parameters.Name.VariablePath.UserPath -contains 'Credential')
            {
                if ($null -eq $taskArguments.Credential -and $null -ne $Task.Credential)
                {
                    $taskArguments.Credential = $Task.Credential
                }
            }

            & $taskScriptBlock @taskArguments -ErrorAction 'Stop'

            $retry = $false
        }
        catch
        {
            Write-Error $_

            # Prepare the retry choices in case of the exception
            $retryChoices = New-Object -TypeName 'Collections.ObjectModel.Collection[Management.Automation.Host.ChoiceDescription]'
            $retryChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Retry', 'Retry this task'))
            $retryChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Continue', 'Continue with next task'))
            $retryChoices.Add((New-Object Management.Automation.Host.ChoiceDescription -ArgumentList '&Stop', 'Stop the maintenance'))

            # Query the desired choice from the user
            do
            {
                $retryResult = $Host.UI.PromptForChoice('Repeat', "Do you want to retry $($Task.Type) $($Task.Name)?", $retryChoices, -1)
            }
            while ($retryResult -eq -1)

            switch ($retryResult)
            {
                0 { $retry = $true }
                1 { $retry = $false }
                2 { throw 'Maintenance stopped by user!' }
            }
        }

        $retryCount++
    }
    while ($retry)
}


function New-AutonanceContainer
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    param
    (
        # The container type, e.g. Maintenance, TaskGroup, etc.
        [Parameter(Mandatory = $true)]
        [System.String]
        $Type,

        # The container name, which will be shown after the container type.
        [Parameter(Mandatory = $false)]
        [System.String]
        $Name = '',

        # The credentials, which will be used for all container tasks.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        $Credential = $null,

        # The script block, which contains the container tasks definitions.
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock,

        # The processing mode of the container. Currently only sequential mode
        # is supported.
        [Parameter(Mandatory = $false)]
        [ValidateSet('Sequential')]
        [System.String]
        $Mode = 'Sequential',

        # Specifies, if the container tasks should be repeated.
        [Parameter(Mandatory = $false)]
        [System.Boolean]
        $Repeat = $false,

        # If the container tasks will be repeated, define the number of repeat
        # loops. Use 0 for an infinite loop.
        [Parameter(Mandatory = $false)]
        [System.Int32]
        $RepeatCount = 0,

        # If the container tasks will be repeated, define the repeat condition.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.ScriptBlock]
        $RepeatCondition = $null,

        # If the container tasks will be repeated, define if the user gets an
        # inquire for every loop.
        [Parameter(Mandatory = $false)]
        [System.Boolean]
        $RepeatInquire = $false
    )

    # Create and return the container object
    [PSCustomObject] [Ordered] @{
        PSTypeName      = 'Autonance.Container'
        Type            = $Type
        Name            = $Name
        Credential      = $Credential
        ScriptBlock     = $ScriptBlock
        Mode            = $Mode
        Repeat          = $Repeat
        RepeatCount     = $RepeatCount
        RepeatCondition = $RepeatCondition
        RepeatInquire   = $RepeatInquire
    }
}


function New-AutonanceSession
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $ComputerName,

        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [System.Management.Automation.PSCredential]
        $Credential,

        [Parameter(Mandatory = $true)]
        [ValidateSet('WinRM', 'CIM')]
        [System.String]
        $SessionType,

        [Parameter(Mandatory = $false)]
        [switch]
        $Silent
    )

    # Session splat
    $sessionSplat = @{}
    if ($null -ne $Credential)
    {
        $sessionSplat['Credential'] = $Credential
        $messageSuffix = " as $($Credential.UserName)"
    }

    if (-not $Silent.IsPresent)
    {
        Write-Autonance -Message "Open $SessionType connection to $ComputerName$messageSuffix"
    }

    # Create a new session
    switch ($SessionType)
    {
        'WinRM'
        {
            if ($PSCmdlet.ShouldProcess($ComputerName, 'Open WinRM session'))
            {
                New-PSSession -ComputerName $ComputerName @sessionSplat -ErrorAction Stop
            }
        }
        'CIM'
        {
            if ($PSCmdlet.ShouldProcess($ComputerName, 'Open CIM session'))
            {
                New-CimSession -ComputerName $ComputerName @sessionSplat -ErrorAction Stop
            }
        }
    }
}


function New-AutonanceTask
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
    param
    (
        # The task type, e.g. LocalScript, WindowsComputerReboot, etc.
        [Parameter(Mandatory = $true)]
        [System.String]
        $Type,

        # The task name, which will be shown after the task type.
        [Parameter(Mandatory = $false)]
        [System.String]
        $Name,

        # The credentials, which will be used for the task.
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]
        $Credential,

        # The script block, which contains the task definition.
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $ScriptBlock,

        # The task arguments to pass for the task execution.
        [Parameter(Mandatory = $false)]
        [System.Collections.Hashtable]
        $Arguments
    )

    # Create and return the task object
    [PSCustomObject] [Ordered] @{
        PSTypeName  = 'Autonance.Task'
        Type        = $Type
        Name        = $Name
        Credential  = $Credential
        ScriptBlock = $ScriptBlock
        Arguments   = $Arguments
    }
}


function Pop-ImpersonationContext
{
    [CmdletBinding()]
    param ()

    Initialize-ImpersonationContext

    if ($Script:ImpersonationContext.Count -gt 0)
    {
        # Get the latest impersonation context
        $popImpersonationContext = $Script:ImpersonationContext.Pop()

        # Undo the impersonation
        $popImpersonationContext.Undo()
    }
}


function Push-ImpersonationContext
{
    [CmdletBinding()]
    param
    (
        # Specifies a user account to impersonate.
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        [System.Management.Automation.Credential()]
        $Credential,

        # The logon type.
        [Parameter(Mandatory = $false)]
        [ValidateSet('Interactive', 'Network', 'Batch', 'Service', 'Unlock', 'NetworkClearText', 'NewCredentials')]
        $LogonType = 'Interactive',

        # The logon provider.
        [Parameter(Mandatory = $false)]
        [ValidateSet('Default', 'WinNT40', 'WinNT50')]
        $LogonProvider = 'Default'
    )

    Initialize-ImpersonationContext

    # Handle for the logon token
    $tokenHandle = [IntPtr]::Zero

    # Now logon the user account on the local system
    $logonResult = [Win32.AdvApi32]::LogonUser($Credential.GetNetworkCredential().UserName,
                                               $Credential.GetNetworkCredential().Domain,
                                               $Credential.GetNetworkCredential().Password,
                                               ([Win32.Logon32Type] $LogonType),
                                               ([Win32.Logon32Provider] $LogonProvider),
                                               [ref] $tokenHandle)

    # Error handling, if the logon fails
    if (-not $logonResult)
    {
        $errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()

        throw "Failed to call LogonUser() throwing Win32 exception with error code: $errorCode"
    }

    # Now, impersonate the new user account
    $newImpersonationContext = [System.Security.Principal.WindowsIdentity]::Impersonate($tokenHandle)
    $Script:ImpersonationContext.Push($newImpersonationContext)

    # Finally, close the handle to the token
    [Win32.Kernel32]::CloseHandle($tokenHandle) | Out-Null
}


function Read-HostMultipleChoiceSelection
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Caption,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Message,

        [Parameter(Mandatory = $true)]
        [System.Object[]]
        $ChoiceObject,

        [Parameter(Mandatory = $true)]
        [System.String[]]
        $ChoiceLabel
    )

    if ($ChoiceObject.Count -ne $ChoiceLabel.Count)
    {
        throw 'ChoiceObject and ChoiceLabel item count do not match.'
    }

    Write-Host ''
    Write-Host $Caption
    Write-Host $Message

    for ($i = 0; $i -lt $ChoiceLabel.Count; $i++)
    {
        Write-Host ('[{0:00}] {1}' -f ($i + 1), $ChoiceLabel[$i])
    }

    Write-Host '(input comma-separated choices or * for all)'

    do
    {
        $rawInputs = Read-Host -Prompt 'Choice'
    }
    while ([String]::IsNullOrWhiteSpace($rawInputs))

    if ($rawInputs -eq '*')
    {
        Write-Output $ChoiceObject
    }
    else
    {
        foreach ($rawInput in $rawInputs.Split(','))
        {
            try
            {
                $rawNumber = [Int32]::Parse($rawInput)

                $rawNumber--

                if ($rawNumber -ge 0 -and $rawNumber -lt $ChoiceLabel.Count)
                {
                    Write-Output $ChoiceObject[$rawNumber]
                }
                else
                {
                    throw
                }
            }
            catch
            {
                Write-Warning "Unable to parse input '$rawInput'"
            }
        }
    }
}


function Remove-AutonanceSession
{
    [CmdletBinding(SupportsShouldProcess = $true)]
    param
    (
        [Parameter(Mandatory = $false)]
        [AllowNull()]
        [System.Object]
        $Session,

        [Parameter(Mandatory = $false)]
        [switch]
        $Silent
    )

    # Remove existing session
    if ($null -ne $Session)
    {
        if ($Session -is [System.Management.Automation.Runspaces.PSSession])
        {
            if (-not $Silent.IsPresent)
            {
                Write-Autonance -Message "Close WinRM connection to $ComputerName"
            }

            if ($PSCmdlet.ShouldProcess($Session, 'Close WinRM session'))
            {
                $Session | Remove-PSSession -ErrorAction SilentlyContinue
            }
        }

        if ($session -is [Microsoft.Management.Infrastructure.CimSession])
        {
            if (-not $Silent.IsPresent)
            {
                Write-Autonance -Message "Close CIM connection to $ComputerName"
            }

            if ($PSCmdlet.ShouldProcess($Session, 'Close COM session'))
            {
                $Session | Remove-CimSession -ErrorAction SilentlyContinue
            }
        }
    }
}


function Wait-AutonanceTask
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Activity,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.ScriptBlock]
        $Condition,

        [Parameter(Mandatory = $true)]
        [System.Int32]
        $Count,

        [Parameter(Mandatory = $true)]
        [System.Int32]
        $Delay
    )

    for ($i = 1; $i -le $Count; $i++)
    {
        Write-Progress -Activity $Activity -Status "$i / $Count" -PercentComplete ($i / $Count * 100) -Verbose

        # Record the timestamp before the condition query
        $timestamp = Get-Date

        # Evaluate the condition and exit the loop, if the result is true
        $result = & $Condition
        if ($result)
        {
            Write-Progress -Activity $Activity -Completed

            return
        }

        # Calculate the remaining sleep duration
        $duration = (Get-Date) - $timestamp
        $leftover = $Delay - $duration.TotalSeconds

        # Sleep if required
        if ($leftover -gt 0)
        {
            Start-Sleep -Seconds $leftover
        }
    }

    Write-Progress -Activity $Activity -Completed

    throw "Timeout: $Activity"
}


function Write-Autonance
{
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')]
    param
    (
        [Parameter(Mandatory = $true, Position = 0)]
        [AllowEmptyString()]
        [System.String]
        $Message,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Container', 'Task', 'Action', 'Info')]
        [System.String]
        $Type = 'Action'
    )

    if (!$Script:AutonanceSilent)
    {
        $messageLines = $Message.Split("`n")

        if ($Script:AutonanceBlock)
        {
            $colorSplat = @{}

            switch ($Type)
            {
                'Container'
                {
                    $prefixFirst = ' ' * $Script:AutonanceLevel
                    $prefixOther = ' ' * $Script:AutonanceLevel

                    $colorSplat['ForegroundColor'] = 'Magenta'
                }
                'Task'
                {
                    $prefixFirst = ' ' * $Script:AutonanceLevel
                    $prefixOther = ' ' * $Script:AutonanceLevel

                    $colorSplat['ForegroundColor'] = 'Magenta'
                }
                'Action'
                {
                    $prefixFirst = ' ' * $Script:AutonanceLevel + ' - '
                    $prefixOther = ' ' * $Script:AutonanceLevel + ' '

                    $colorSplat['ForegroundColor'] = 'Cyan'
                }
                'Info'
                {
                    $prefixFirst = ' ' * $Script:AutonanceLevel
                    $prefixOther = ' ' * $Script:AutonanceLevel
                }
            }

            $messageLines = "$prefixFirst$($messageLines -join "`n$prefixOther")".Split("`n")

            if ($Type -eq 'Info')
            {
                $messageLines | Write-Host
            }
            else
            {
                for ($i = 0; $i -lt $messageLines.Count; $i++)
                {
                    if ($null -eq $Script:AutonanceTimestamp)
                    {
                        $Script:AutonanceTimestamp = Get-Date

                        $timestamp = '{0:dd.MM.yyyy HH:mm:ss} 00:00:00' -f $Script:AutonanceTimestamp
                    }
                    else
                    {
                        $timestamp = ((Get-Date) - $Script:AutonanceTimestamp).ToString('hh\:mm\:ss')
                    }

                    $timestampWidth = $Host.UI.RawUI.WindowSize.Width - $messageLines[$i].Length - 4

                    if ($i -eq 0 -and $timestampWidth -ge $timestamp.Length)
                    {
                        Write-Host -Object $messageLines[$i] @colorSplat -NoNewline
                        Write-Host -Object (" {0,$timestampWidth}" -f $timestamp) -ForegroundColor 'DarkGray'
                    }
                    else
                    {
                        Write-Host -Object $messageLines[$i] @colorSplat
                    }
                }
            }
        }
        else
        {
            foreach ($messageLine in $messageLines)
            {
                Write-Verbose $messageLine
            }
        }
    }
}


# Initialize context variables
$Script:AutonanceBlock     = $false
$Script:AutonanceLevel     = 0
$Script:AutonanceSilent    = $false
$Script:AutonanceExtension = @{}
$Script:AutonanceTimestamp = $null

$Script:ModulePath = $PSScriptRoot