Modules/businessdev.ALbuild.Containers/Public/Enter-BcContainer.ps1

function Enter-BcContainer {
    <#
    .SYNOPSIS
        Opens an interactive PowerShell session inside a Business Central container.
 
    .DESCRIPTION
        The equivalent of BcContainerHelper's Enter-BcContainer / Open-BcContainer: a prompt inside the
        container, for the times when you need to look rather than automate - reading a service tier log,
        checking what a failed install actually left behind, running Get-NAVAppInfo by hand.
 
        WHY THIS IS NOT Invoke-BcContainerCommand
        Invoke-BcContainerCommand captures stdout and stderr, which is exactly right for automation and
        exactly wrong for a session: a captured stream draws no prompt and accepts no keystrokes. This
        hands the console to 'docker exec -it' and gets out of the way, so the exit code you see is the
        shell's own.
 
        The in-container host is resolved by Get-BcContainerPowerShellExe, so this lands on the shell
        that can actually load the BC management cmdlets - 'powershell' up to BC28, 'pwsh' from BC29,
        where the Windows PowerShell compatibility layer was removed.
 
        Refuses to start without a terminal. 'docker exec -it' on a redirected stdin either fails with
        "the input device is not a TTY" or waits forever, and a pipeline job that hangs on an interactive
        prompt is far worse than one that says why it cannot run - so this checks first and points at
        Invoke-BcContainerCommand instead.
 
    .PARAMETER Name
        The container to enter.
 
    .PARAMETER Command
        A command to run before the prompt appears. The session stays open afterwards.
 
    .PARAMETER WorkingDirectory
        Where the session starts. Default 'C:\run', which is where the generic image keeps its scripts.
 
    .PARAMETER PowerShellExe
        Pin the in-container shell instead of letting Get-BcContainerPowerShellExe choose it.
 
    .PARAMETER SkipPrompt
        Do not dot-source the image's C:\Run\prompt.ps1. Use it when that script gets in the way.
 
    .PARAMETER DockerExecutable
        The Docker executable to use. Default 'docker'.
 
    .EXAMPLE
        Enter-BcContainer -Name bcserver
 
        A prompt inside 'bcserver', in C:\run, with the image's prompt loaded.
 
    .EXAMPLE
        Enter-BcContainer -Name bcserver -Command 'Get-NAVAppInfo -ServerInstance BC'
 
        Runs the command, then leaves the session open to keep looking.
 
    .EXAMPLE
        Open-BcContainer bcserver
 
        The BcContainerHelper name, kept as an alias.
 
    .OUTPUTS
        None. The exit code of the in-container shell is left in $LASTEXITCODE.
    #>

    [CmdletBinding()]
    [Alias('Open-BcContainer')]
    [OutputType([void])]
    param(
        [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [Alias('ContainerName')] [string] $Name,
        [string] $Command,
        [string] $WorkingDirectory = 'C:\run',
        [ValidateSet('powershell', 'pwsh')] [string] $PowerShellExe,
        [switch] $SkipPrompt,
        [string] $DockerExecutable = 'docker'
    )

    # --- preflight: every failure here is cheaper than a hung prompt -----------------------------
    # A session needs a real terminal on both ends. Checked before Docker is even asked, because this
    # is the failure a build agent would otherwise hit, and it would hit it by hanging.
    if ([System.Console]::IsInputRedirected -or [System.Console]::IsOutputRedirected) {
        throw "Enter-BcContainer needs an interactive terminal, and this session's input or output is redirected ('docker exec -it' would fail or hang). Use Invoke-BcContainerCommand to run something in '$Name' non-interactively."
    }

    $null = Test-BcDocker -DockerExecutable $DockerExecutable -Require

    $container = Get-BcContainer -Name $Name -DockerExecutable $DockerExecutable -ErrorAction SilentlyContinue
    if (-not $container) {
        throw "There is no container named '$Name'. 'Get-BcContainer' lists what is on this host."
    }
    if (-not $container.Running) {
        throw "Container '$Name' is $($container.Status), and a shell can only be opened in a running container. Start it with 'Start-BcContainer -Name $Name'."
    }

    $psExe = if ($PowerShellExe) { $PowerShellExe }
    else { Get-BcContainerPowerShellExe -Name $Name -DockerExecutable $DockerExecutable }

    $arguments = Get-BcContainerEnterArgument -ContainerName $Name -PowerShellExe $psExe `
        -Command $Command -WorkingDirectory $WorkingDirectory -SkipPrompt:$SkipPrompt

    Write-ALbuildLog "Entering '$Name' with $psExe. Type 'exit' to return."

    # Called directly, NOT through Invoke-BcDocker: that captures the streams, and a captured stream
    # cannot be typed into. The native call inherits this console, which is the whole point.
    & $DockerExecutable @arguments
    $exit = $LASTEXITCODE

    # 130 is SIGINT (Ctrl+C) and is how a session is normally left - not a failure worth a warning.
    if ($exit -ne 0 -and $exit -ne 130) {
        Write-ALbuildLog -Level Warning "The session in '$Name' ended with exit code $exit."
    }
}