goexit-codesigint

Exit code 130 on Linux and 2 on Windows on SIGINT


Why is the following empty program exiting on Ctrl + C with 130 on Linux (which is what I suspect, because my shell bash wraps SIGINT to 130 (128+2).

On Windows with Git Bash (git-bash.exe), I get exit code 2.

package main

func main() {
    for {

    }
}

Is that Go's behavior on Windows or git-bash.exe? Because I need exit code 2 internally, do I need to wrap it using the signal package?


Solution

  • Well, it's two-fold.

    On the one hand, as @Flimzy pointed out, it's shell intervening.

    On the other hand, what is missing from his remark is why this happens.
    The explanation is, again, two-fold:


    Update on the behaviour in Windows.

    Let's first put up a quick fact sheet:

    Let's now dig a bit deeper.

    Unix was born without any notion of GUI and the users would interact with a Unix system using hardware terminals. In order to support them, kernels of Unix-like OSes implement special standardized way to make terminal-aware programs interact with the system; if you're "into" deep-diving into technical details, I highly recommend reading the "TTY demystified" piece.
    The two more important highlights of this approach are:

    The latter is of particular interest. You can run stty -a in a terminal window on your Linux system; amoung the copious output you'd see something like intr = ^C; quit = ^\; which means Ctrl-C sends interactive attention (SIGINT) signal and Ctrl-\ sends SIGQUIT (yes, "INT" in "SIGINT" does not stand for "interrupt"—contrary to a popular belief).
    You could reassign these key combos almost at will (though it's not a wise thing to do as many pieces of software expect ^C and ^\ to be mapped the way they usually do and do not assign their own actions to these gestures—rightfully expecting to not be able to actually ever receive them.

    Now back to Windows.
    On Windows, there is no terminal subsystem, and no signals. Console window on Windows was an artefact required to provide compatibility with the older MS-DOS system, and there the situation was like this: Ctrl-Break would trigger a hardware interrupt usually handled by the OS, and Ctrl-C could be explicitly enabled to do the same. The implementation of the console emulation on Windows carefully emulated this behaviour, but since Windows does not have Unix-like signals, the handling of these keyboard combos is done differently—though with much the same effect:

    Each console process has its own list of application-defined HandlerRoutine functions that handle CTRL+C and CTRL+BREAK signals. The handler functions also handle signals generated by the system when the user closes the console, logs off, or shuts down the system. Initially, the handler list for each process contains only a default handler function that calls the ExitProcess function.

    What this all means to Go?

    Let's first see the docs:

    ~$ GOOS=windows go doc os.Interrupt

    package os // import "os"
    
    var (
      Interrupt Signal = syscall.SIGINT
      Kill      Signal = syscall.SIGKILL
    )
    

    The only signal values guaranteed to be present in the os package on all systems are os.Interrupt (send the process an interrupt) and os.Kill (force the process to exit). On Windows, sending os.Interrupt to a process with os.Process.Signal is not implemented; it will return an error instead of sending a signal.

    So, in a Go program running on Windows you can handle these two "signals"—even though they were not really be implemented as signals.

    Let's now move to explaning the difference in the exit codes.

    As you know by now, pressing Ctrl-C when a program is running in a terminal emulator windows on a Unix-like system will make the terminal subsystem send the process the actual SIGINT signal. If this signal is not explicitly handled, the process gets killed by the OS (as that's what the default signal disposition says). The shell notices that a process it spawned suddenly died, collects its exit code and adds 128 to it (because it wasn't expecting it to die that way).
    On Windows, hitting Ctrl-C makes the process perform the ExitProcess system call, which, form the point of view of the shell process looks like normal process exit: it cannot tell this exit apart from the one occured if the process were to call os.Exit(0) explicitly.