dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
15.06k stars 4.69k forks source link

Canceling the `PosixSignal.SIGTSTP` does not seem to be handled correctly #78302

Open bmitc opened 1 year ago

bmitc commented 1 year ago

Description

I am writing a console application in F# using .NET 7. I am wanting to intercept and prevent console exit signals, such as PosixSignal.SIGTSTP and PosixSignal.SIGINT. These enums are defined here. To ignore them, I create and register a handler using PosixSignalRegistration.Create and then write true to the Cancel property inside the handler. This should cancel the signal, to my understanding, as it does for PosixSignal.SIGINT. However, that doesn't seem to be the case for PosixSignal.SIGTSTP.

Reproduction Steps

These steps were executed on Ubuntu 20 running in WSL2 on Windows 11.

  1. Save the following code into an F# script named posix.fsx.

    // posix.fsx
    open System
    open System.Runtime.InteropServices
    
    PosixSignalRegistration.Create(PosixSignal.SIGINT,
        fun context ->
            printfn "Prevented Posix SIGINT"
            context.Cancel <- true)
    |> ignore
    
    if System.OperatingSystem.IsLinux() then
        printfn "System is Linux"
        PosixSignalRegistration.Create(PosixSignal.SIGTSTP,
            fun context ->
                printfn "Prevented Posix SIGTSTP"
                context.Cancel <- true)
        |> ignore
    
    let mutable cont = true
    
    while cont do
        let key = Console.ReadKey(intercept = true)
        let keyChar = key.KeyChar
        let keyModifiers = key.Modifiers
        printfn "key: %A, modifiers: %A" keyChar keyModifiers
        if keyChar = 'q' then
            cont <- false
  2. Run the script with dotnet fsi posix.fsx

  3. Type any character except 'q'. The key information will be printed out.

  4. Enter Ctrl + C and note that "Prevented Posix SIGINT" is printed out to the screen but that the process is still running, which can be verified by entering in other characters (again, besides 'q').

  5. Enter Ctrl + Z and note that:

    • [3]+ Stopped dotnet fsi posix.fsx is printed to the screen
    • The process is stopped, but then "Prevented Posix SIGTSTP" is printed out afterwards, after the new prompt is shown.
    • However, the process seems to be restarted or still going, as it will respond to a key entry but usually only for one, and the process will end when Enter is entered. This behavior is a little inconsistent.

Expected behavior

When Ctrl + Z is entered, "Prevented Posix SIGTSTP" should be printed to the console without the program being stopped.

Actual behavior

$ dotnet fsi posix.fsx
System is Linux
key: 'a', modifiers: 0
key: '\013', modifiers: 0
Prevented Posix SIGINT
key: 'a', modifiers: 0

[2]+  Stopped                 dotnet fsi posix.fsx
$ Prevented Posix SIGTSTP
key: 'a', modifiers: 0

$ 

Regression?

Not sure.

Known Workarounds

None that I know of.

Configuration

$ dotnet --version
7.0.100

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.5 LTS
Release:        20.04
Codename:       focal

The architecture is x64.

Other information

No response

dotnet-issue-labeler[bot] commented 1 year ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

ghost commented 1 year ago

Tagging subscribers to this area: @dotnet/interop-contrib See info in area-owners.md if you want to be subscribed.

Issue Details
### Description I am writing a console application in F# using .NET 7. I am wanting to intercept and prevent console exit signals, such as `PosixSignal.SIGTSTP` and `PosixSignal.SIGINT`. These [enums are defined here](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignal?view=net-7.0). To ignore them, I create and register a handler using [`PosixSignalRegistration.Create`](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.posixsignalregistration.create?view=net-7.0) and then write `true` to the `Cancel` property inside the handler. This should cancel the signal, to my understanding, as it does for `PosixSignal.SIGINT`. However, that doesn't seem to be the case for `PosixSignal.SIGTSTP`. ### Reproduction Steps These steps were executed on Ubuntu 20 running in WSL2 on Windows 11. 1. Save the following code into an F# script named `posix.fsx`. ```fsharp // posix.fsx open System open System.Runtime.InteropServices PosixSignalRegistration.Create(PosixSignal.SIGINT, fun context -> printfn "Prevented Posix SIGINT" context.Cancel <- true) |> ignore if System.OperatingSystem.IsLinux() then printfn "System is Linux" PosixSignalRegistration.Create(PosixSignal.SIGTSTP, fun context -> printfn "Prevented Posix SIGTSTP" context.Cancel <- true) |> ignore let mutable cont = true while cont do let key = Console.ReadKey(intercept = true) let keyChar = key.KeyChar let keyModifiers = key.Modifiers printfn "key: %A, modifiers: %A" keyChar keyModifiers if keyChar = 'q' then cont <- false ``` 2. Run the script with `dotnet fsi posix.fsx` 3. Type any character except `'q'`. The key information will be printed out. 4. Enter `Ctrl + C` and note that `"Prevented Posix SIGINT"` is printed out to the screen but that the process is still running, which can be verified by entering in other characters (again, besides `'q'`). 5. Enter `Ctrl + Z` and note that: * `[3]+ Stopped dotnet fsi posix.fsx` is printed to the screen * The process is stopped, but then `"Prevented Posix SIGTSTP"` is printed out afterwards, after the new prompt is shown. * However, the process *seems* to be restarted or still going, as it will respond to a key entry but usually only for one, and the process will end when `Enter` is entered. This behavior is a little inconsistent. ### Expected behavior When `Ctrl + Z` is entered, `"Prevented Posix SIGTSTP"` should be printed to the console without the program being stopped. ### Actual behavior ```bash $ dotnet fsi posix.fsx System is Linux key: 'a', modifiers: 0 key: '\013', modifiers: 0 Prevented Posix SIGINT key: 'a', modifiers: 0 [2]+ Stopped dotnet fsi posix.fsx $ Prevented Posix SIGTSTP key: 'a', modifiers: 0 $ ``` ### Regression? Not sure. ### Known Workarounds None that I know of. ### Configuration ```bash $ dotnet --version 7.0.100 $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04.5 LTS Release: 20.04 Codename: focal ``` The architecture is x64. ### Other information _No response_
Author: bmitc
Assignees: -
Labels: `area-System.Runtime.InteropServices`, `untriaged`
Milestone: -
tmds commented 1 year ago

This should cancel the signal

The semantics of Cancel depend on the signal and what use-case we want to enable.

SIGTSTP was added on request of @alexrp (https://github.com/dotnet/runtime/issues/50527#issuecomment-812840192). We didn't diverge much into his specific use-case when including it.

We should look at the use-case and see what is the meaningful behavior when Cancel is true/false.

Looking at the code, I think they currently both do the same thing: prevent SIGTSTP from stopping the app.

alexrp commented 1 year ago

My understanding (and reason for bringing it up back then) is that the common use case for handling SIGTSTP is to save any critical program state before it's suspended. I would expect a program to do that in the event handler, so no need to set Cancel = true.

I can't really think of why one would want to completely block SIGTSTP. Seeing as SIGSTOP can't be blocked anyway, there's no way to 'opt out' of suspension altogether. In any case, I would expect Cancel to do the intuitive thing: true blocks the signal, false lets it go through (so the program gets suspended).

tmds commented 1 year ago

I would expect Cancel to do the intuitive thing: true blocks the signal, false lets it go through (so the program gets suspended).

We can change it to do that, which is a breaking change, but probably the desired behavior.

When at its default value of Cancel = false, the process will then be stopped. (by sending SIGSTOP to it after the handler ran). When set to Cancel = true, the process will keep running.

@bmitc how does that sound to you?

bmitc commented 1 year ago

I apologize for getting back to this so late. This is a side project that got pushed aside. Thank you @tmds and @alexrp for responding so quickly to this, and I'm again sorry for the delay.

My use case was using pure F# to implement the modified Kilo text editor described in Build Your Own Text Editor. Specifically, it was in implementing the section Turn off Ctrl-C and Ctrl-Z signals where I came across this issue. The tutorial uses the C library termios to accomplish disabling SIGINT and SIGTSTP. For reference (also in the above link):

By default, Ctrl-C sends a SIGINT signal to the current process which causes it to terminate, and Ctrl-Z sends a SIGTSTP signal to the current process which causes it to suspend. Let’s turn off the sending of both of these signals.

But to do this behavior in pure F#/.NET, I was needing the PosixSignal.SIGINT and PosixSignal.SIGTSTP signals actually canceled when doing context.Cancel <- true. I think a terminal text editor is probably a legitimate use case for canceling these, although admittedly is probably a rare one.

However, I am not remotely close to an expert on terminal implementations, which is one reason why I was working through this tutorial, so it's possible I misunderstand something here.

tmds commented 1 year ago

Specifically, it was in implementing the section Turn off Ctrl-C and Ctrl-Z signals where I came across this issue. The tutorial uses the C library termios to accomplish disabling SIGINT and SIGTSTP. For reference (also in the above link):

Besides adding the PosixSignalRegistration, you should also set Console.TreatControlCAsInput to true.

Then you'll be able to read the Ctrl-C and Ctrl-Z combinations.

KeyChar will be the raw control value as mentioned in the article (Now Ctrl-C can be read as a 3 byte and Ctrl-Z can be read as a 26 byte.). ConsoleKey will be set based on that.

jkoritzinsky commented 1 year ago

Moving this to .NET 9 as the likely fix is a breaking change and we're pretty late into .NET 8 for breaking changes.

jkoritzinsky commented 2 months ago

Sadly we didn't get to this in .NET 9 and it is likely still a breaking change, so moving to .NET 10.