7

I'm writing a monitoring script in Powershell using a Try/Finally to log a message should the script end. The script is intended to run indefinitely, so I want a way to track unintended exiting.

Every other StackOverflow post and Help page I've checked states:

A Finally block runs even if you use CTRL+C to stop the script. A Finally block also runs if an Exit keyword stops the script from within a Catch block.

In practice, I have not found this to be true. I'm using the following contrived example to test this out:

Try {
    While($True) {
        echo "looping"
        Start-Sleep 3
    }
} Finally {
    echo "goodbye!"
    pause
}

The Finally block here is skipped every time after a Ctrl+C (no echo, no pause), both when running as a saved script or when executing through the built-in Powershell ISE. The only output I ever get is:

looping
looping
...(repeat until Ctrl-C)

I've clearly missed something, but I have no idea what it is, especially in a code snippet as small as this.

Mwiza
  • 7,780
  • 3
  • 46
  • 42
b_c
  • 1,202
  • 13
  • 24
  • 3
    From the very same page: "Note that pressing CTRL+C stops the pipeline. Objects that are sent to the pipeline will not be displayed as output. Therefore, if you include a statement to be displayed, such as "Finally block has run", it will not be displayed after you press CTRL+C, even if the Finally block ran." Try replacing `echo` with `$finallyRan = true`, or other statements that don't generate pipeline output. – Jeroen Mostert Jul 14 '17 at 13:44
  • 1
    Weird, but if I swap `echo` for `Write-Host`, your script works normally. Check if echo is really needed in there. – Vesper Jul 14 '17 at 13:47
  • As an aside, deterministic cleanup *inside* a pipeline is [really unnecessarily difficult](https://stackoverflow.com/questions/28522507/) precisely because of this pipeline-stopping abilitiy, simply relying on `finally` won't do it. – Jeroen Mostert Jul 14 '17 at 13:54
  • @Vesper Echo is an alias for Write-Output which spews to the Output (&1) Stream. Write-Host does not write to any stream – Maximilian Burszley Jul 14 '17 at 14:28
  • @TheIncorrigible1 See, why does `pause` not work if put after `echo` in the finally block? Alone, it works. – Vesper Jul 14 '17 at 14:32
  • @JeroenMostert Wow...I can't believe I missed that :( I was using echo with the intent of redirecting to a logfile, but I suppose I'll have to explore other options. – b_c Jul 17 '17 at 15:01
  • 1
    @b_c Not really... `"goodbye!" | Out-file logfile.txt` works just fine. – G42 Jul 20 '17 at 12:18

2 Answers2

10

The proper answer is that Ctrl+C breaks pipeline, as is also stated in that link, and echo uses the pipeline to process its output. Therefore, once you Ctrl+C, writing to the pipeline causes the script block to err, and to not process any further commands. Therefore, do not use commands that send output to stdout directly, and there is a lot of them that indirectly use pipeline. Write-Host, on the other hand, does not use the pipeline, and thus does not throw an error.

Mwiza
  • 7,780
  • 3
  • 46
  • 42
Vesper
  • 18,599
  • 6
  • 39
  • 61
2

Functional Code

This will give you the behaviour I believe you're after:

Try {
    While($True) {
        echo "looping"
        Start-Sleep 3
    }
} Finally {
    Write-Host "goodbye!"
    pause
}

References

Write-Output/echo - Synopsis

Sends the specified objects to the next command in the pipeline. If the command is the last command in the pipeline, the objects are displayed in the console.

Write-Host - Synopsis

Writes customized output to a host.

Try-Catch-Finally - Syntax note

Note that pressing CTRL+C stops the pipeline. Objects that are sent to the pipeline will not be displayed as output. Therefore, if you include a statement to be displayed, such as "Finally block has run", it will not be displayed after you press CTRL+C, even if the Finally block ran.

Explanation

The key, as per TheIncorrigible1's comment and Vesper's answer is that the pipeline is stopped. But this is not because of an error in Write-Output. And I don't find it is a satisfying explanation on its own.

  • "If the command is the last command in the pipeline, the objects are displayed in the console." - appears this statement is false within a finally block. However, passing to Out-Host explicitly will yield desired output.
  • On Try-Catch-Finally note
    • The quoted section is confusing as it applies to unhandled objects sent to the pipeline.
    • Objects sent to the pipeline and handled within a Finally block are fine.
    • It talks about "even if the Finally block has ran" but the pause does not run if preceded by a Write-Output.

More Code

A few things ran in the Finally block to investigate behaviour, with comments as to what happens.

} Finally {
    Write-Output "goodbye!" | Out-Default # works fine
    pause
}

} Finally {
    Write-Output "goodbye!" | Out-Host    # works fine
    pause
}

} Finally {
    pause                     # works fine
    Write-output "goodbye!"   # not executed
}

} Finally {
    try{
        Write-Output "goodbye!" -ErrorAction Stop
    }catch{
        Write-Host "error caught"   # this is not executed.
    }                               # $error[0] after script execution is empty
    pause
}

} Finally {
    try{
        ThisCommandDoesNotExist
    }catch{
        Write-Host "error caught"   # this is executed
    }                               # $error[0] contains CommandNotFoundException      
    pause
}
G42
  • 9,791
  • 2
  • 19
  • 34
  • 1
    I assume that the raw script block output is still passed to `Out-Default`, there you have a pipeline, and there is the reason of why it gets broken. But, why does `pause` not work? – Vesper Jul 14 '17 at 13:50
  • @Vesper `pause` on its own works. I assume that because the `Write-Output` part does not (still trying to figure out exactly _why_), the rest of the Finally block is not executed. Do you mean that `Write-Output` passes contents as object to `Out-Default`? **edit: (again)** Can you please explain `Out-Default` with regard to `Write-Output`? – G42 Jul 14 '17 at 13:52
  • 2
    Yes, and this somehow really breaks the code execution, that is, `echo "goodbye"` throws some (not really sure which) error that makes the rest of finally block be skipped. Wrapping the code into another try-catch does NOT work, but wrapping into try-finally DOES make "finally" block work. There's something weird with Powershell processing a Ctrl+C sequence. – Vesper Jul 14 '17 at 14:03
  • 1
    @Vesper Read [the last paragraph](https://msdn.microsoft.com/powershell/reference/5.1/Microsoft.PowerShell.Core/about/about_Try_Catch_Finally#syntax). The pipeline is stopped. – Maximilian Burszley Jul 14 '17 at 14:32