Search code examples
powershellscriptingpowershell-corepowershell-7.0

How to stop a child process with Ctrl+C without breaking the main script and stream child output to the main script in PowerShell?


I am writing a powershell script that is running on Linux. The purpose of this script is to run the MS SQL Server and show its output, but when the user presses Ctrl+C or some error happens, the script takes a copy of the data folder and then after that exits.

[CmdletBinding()]
PARAM (
    [Parameter(ValueFromPipelineByPropertyName)]
    [string] $Command,

    [Parameter(ValueFromPipelineByPropertyName)]
    [string] $Args
)
BEGIN {
    Write-Output "started $PSScriptRoot"
    $currentErrorLevel = $ErrorActionPreference
    # [console]::TreatControlCAsInput = $true
}
PROCESS {

    try {
        $ErrorActionPreference = 'SilentlyContinue';
        $IsSqlRunning = Get-Process -name "sqlservr";

        if ($null -eq $IsSqlRunning) {
            start-process "/opt/mssql/bin/sqlservr" -wait -NoNewWindow
        }
    }
    catch {
        $ErrorActionPreference = $currentErrorLevel;
        Write-Error $_.Exception
    }

}
End {
    $ErrorActionPreference = $currentErrorLevel;
    #do backup
    Create-Backup "/opt/mssql/bin/data"
    Write-Output "finishd!"
}

I got a couple of problems with this script:

  • When I press Ctrl+C it breaks the main script and it never reaches to the Create-Backup section at the bottom of the script.
  • If I remove -Wait then the script won't show the sql log output

So my prefered solution is to run the sql with -Wait parameter and prevent the powershell to exit the code after I press Ctrl+C, but instead Ctrl+C close the sql instance

So I'm looking for a way to achieve both.


Solution

  • For simplicity I'll assume that your function only needs to support a single input object, so I'm using a simple function body without begin, process and end blocks, which is equivalent to having just an end block:

    [CmdletBinding()]
    PARAM (
        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $Command,
    
        [Parameter(ValueFromPipelineByPropertyName)]
        [string] $Args
    )
        Write-Output "started $PSScriptRoot"
    
        # No need to save the current value, because the modified
        # value is local to the function and goes out of scope on 
        # exiting the function.
        $ErrorActionPreference = 'SilentlyContinue';
    
        try {
            $IsSqlRunning = Get-Process -name "sqlservr";
    
            if ($null -eq $IsSqlRunning) {
                start-process "/opt/mssql/bin/sqlservr" -wait -NoNewWindow
            }
        }
        catch {
            Write-Error $_.Exception
        }
        finally {
    
          # This block is *always* called - even if Ctrl-C was used.
    
          Create-Backup "/opt/mssql/bin/data"
          # CAVEAT: If Ctrl-C was used to terminate the run, 
          #         you can no longer produce *pipeline* input at this point
          #         (it will be quietly ignored).
          #         However, you can still output to the *host*.
          Write-Host "finished!"
    
        }
    

    If you really need to support multiple input objects, it gets more complicated.