Commit f59b0056d30c6db928268703da8492a0aaed3608

Ryan C. Gordon 2018-08-07T16:56:46

evdev: On sudden termination, make sure keyboard isn't lost (thanks, Tadek!) "In release 2.0.6, when Linux evdev keyboard support has been moved to a separate source file, a feature was added to disable normal keyboard event processing to prevent "spilling" keystrokes to background virtual console. This feature has one unpleasant side effect: if application fails to call `SDL_Exit` before termination or crashes with fatal signal, console is left in unusable state with keyboard not working and no possibility to switch virtual console. If user has a chance, he can login remotely and restore keyboard with `kbd_mode`, otherwise the only option is to reboot the machine. This patch fixes that problem by intercepting fatal signals (with `sigaction`) and process termination (with `atexit`), to restore keyboard state, if it wasn't properly restored with `SDL_Exit`. The function registered with `atexit` also restores original signal handlers, to prevent leaving invalid handlers after SDL library is unloaded, if it was loaded dynamically with `dlopen`. No signal handlers or `atexit` function are installed if SDL boolean hint `SDL_HINT_NO_SIGNAL_HANDLERS` is `SDL_TRUE`. Additionally, if environment variable `SDL_INPUT_LINUX_KEEP_KBD` exists, keyboard initialization function completely skips disabling keyboard. This can be useful for debugging." Fixes Bugzilla #4193.

diff --git a/src/core/linux/SDL_evdev_kbd.c b/src/core/linux/SDL_evdev_kbd.c
index 8b1d524..4359136 100644
--- a/src/core/linux/SDL_evdev_kbd.c
+++ b/src/core/linux/SDL_evdev_kbd.c
@@ -21,6 +21,7 @@
 #include "../../SDL_internal.h"
 
 #include "SDL_evdev_kbd.h"
+#include "SDL_hints.h"
 
 #ifdef SDL_INPUT_LINUXKD
 
@@ -34,6 +35,8 @@
 #include <linux/vt.h>
 #include <linux/tiocl.h> /* for TIOCL_GETSHIFTSTATE */
 
+#include <signal.h>
+
 #include "../../events/SDL_events_c.h"
 #include "SDL_evdev_kbd_default_accents.h"
 #include "SDL_evdev_kbd_default_keymap.h"
@@ -191,6 +194,151 @@ static int SDL_EVDEV_kbd_load_keymaps(SDL_EVDEV_keyboard_state *kbd)
     return 0;
 }
 
+static SDL_EVDEV_keyboard_state * kbd_cleanup_state = NULL;
+static int kbd_cleanup_sigactions_installed = 0;
+static int kbd_cleanup_atexit_installed = 0;
+
+static struct sigaction old_sigaction[NSIG] = { 0 };
+
+static int fatal_signals[] =
+{
+    /* Handlers for SIGTERM and SIGINT are installed in SDL_QuitInit. */
+    SIGHUP,  SIGQUIT, SIGILL,  SIGABRT,
+    SIGFPE,  SIGSEGV, SIGPIPE, SIGBUS,
+    SIGSYS
+};
+
+static void kbd_cleanup(void)
+{
+    SDL_EVDEV_keyboard_state* kbd = kbd_cleanup_state;
+    if (kbd == NULL) {
+        return;
+    }
+    kbd_cleanup_state = NULL;
+
+    fprintf(stderr, "(SDL restoring keyboard) ");
+    ioctl(kbd->console_fd, KDSKBMODE, kbd->old_kbd_mode);
+}
+
+void
+SDL_EVDEV_kbd_reraise_signal(int sig)
+{
+    raise(sig);
+}
+
+siginfo_t* SDL_EVDEV_kdb_cleanup_siginfo = NULL;
+void*      SDL_EVDEV_kdb_cleanup_ucontext = NULL;
+
+static void kbd_cleanup_signal_action(int signum, siginfo_t* info, void* ucontext)
+{
+    struct sigaction* old_action_p = &(old_sigaction[signum]);
+    sigset_t sigset;
+
+    /* Restore original signal handler before going any further. */
+    sigaction(signum, old_action_p, NULL);
+
+    /* Unmask current signal. */
+    sigemptyset(&sigset);
+    sigaddset(&sigset, signum);
+    sigprocmask(SIG_UNBLOCK, &sigset, NULL);
+
+    /* Save original signal info and context for archeologists. */
+    SDL_EVDEV_kdb_cleanup_siginfo = info;
+    SDL_EVDEV_kdb_cleanup_ucontext = ucontext;
+
+    /* Restore keyboard. */
+    kbd_cleanup();
+
+    /* Reraise signal. */
+    SDL_EVDEV_kbd_reraise_signal(signum);
+}
+
+static void kbd_unregister_emerg_cleanup()
+{
+    int tabidx, signum;
+
+    kbd_cleanup_state = NULL;
+
+    if (!kbd_cleanup_sigactions_installed) {
+        return;
+    }
+    kbd_cleanup_sigactions_installed = 0;
+
+    for (tabidx = 0; tabidx < sizeof(fatal_signals) / sizeof(fatal_signals[0]); ++tabidx) {
+        struct sigaction* old_action_p;
+        struct sigaction cur_action;
+        signum = fatal_signals[tabidx];
+        old_action_p = &(old_sigaction[signum]);
+
+        /* Examine current signal action */
+        if (sigaction(signum, NULL, &cur_action))
+            continue;
+
+        /* Check if action installed and not modifed */
+        if (!(cur_action.sa_flags & SA_SIGINFO)
+                || cur_action.sa_sigaction != &kbd_cleanup_signal_action)
+            continue;
+
+        /* Restore original action */
+        sigaction(signum, old_action_p, NULL);
+    }
+}
+
+static void kbd_cleanup_atexit(void)
+{
+    /* Restore keyboard. */
+    kbd_cleanup();
+
+    /* Try to restore signal handlers in case shared library is being unloaded */
+    kbd_unregister_emerg_cleanup();
+}
+
+static void kbd_register_emerg_cleanup(SDL_EVDEV_keyboard_state * kbd)
+{
+    int tabidx, signum;
+
+    if (kbd_cleanup_state != NULL) {
+        return;
+    }
+    kbd_cleanup_state = kbd;
+
+    if (!kbd_cleanup_atexit_installed) {
+        /* Since glibc 2.2.3, atexit() (and on_exit(3)) can be used within a shared library to establish
+         * functions that are called when the shared library is unloaded.
+         * -- man atexit(3)
+         */
+        atexit(kbd_cleanup_atexit);
+        kbd_cleanup_atexit_installed = 1;
+    }
+
+    if (kbd_cleanup_sigactions_installed) {
+        return;
+    }
+    kbd_cleanup_sigactions_installed = 1;
+
+    for (tabidx = 0; tabidx < sizeof(fatal_signals) / sizeof(fatal_signals[0]); ++tabidx) {
+        struct sigaction* old_action_p;
+        struct sigaction new_action;
+        signum = fatal_signals[tabidx];   
+        old_action_p = &(old_sigaction[signum]);
+        if (sigaction(signum, NULL, old_action_p))
+            continue;
+
+        /* Skip SIGHUP and SIGPIPE if handler is already installed
+         * - assume the handler will do the cleanup
+         */
+        if ((signum == SIGHUP || signum == SIGPIPE)
+                && (old_action_p->sa_handler != SIG_DFL 
+                    || (void (*)(int))old_action_p->sa_sigaction != SIG_DFL))
+            continue;
+
+        new_action = *old_action_p;
+        new_action.sa_flags |= SA_SIGINFO;
+        new_action.sa_sigaction = &kbd_cleanup_signal_action;
+        sigaction(signum, &new_action, NULL);
+    }
+}
+
 SDL_EVDEV_keyboard_state *
 SDL_EVDEV_kbd_init(void)
 {
@@ -238,10 +386,20 @@ SDL_EVDEV_kbd_init(void)
             kbd->key_maps = default_key_maps;
         }
 
-        /* Mute the keyboard so keystrokes only generate evdev events
-         * and do not leak through to the console
-         */
-        ioctl(kbd->console_fd, KDSKBMODE, K_OFF);
+        /* Allow inhibiting keyboard mute with env. variable for debugging etc. */
+        if (getenv("SDL_INPUT_LINUX_KEEP_KBD") == NULL) {
+            /* Mute the keyboard so keystrokes only generate evdev events
+             * and do not leak through to the console
+             */
+            ioctl(kbd->console_fd, KDSKBMODE, K_OFF);
+
+            /* Make sure to restore keyboard if application fails to call
+             * SDL_Quit before exit or fatal signal is raised.
+             */
+            if (!SDL_GetHintBoolean(SDL_HINT_NO_SIGNAL_HANDLERS, SDL_FALSE)) {
+                kbd_register_emerg_cleanup(kbd);
+            }
+        }
     }
 
 #ifdef DUMP_ACCENTS
@@ -260,6 +418,8 @@ SDL_EVDEV_kbd_quit(SDL_EVDEV_keyboard_state *kbd)
         return;
     }
 
+    kbd_unregister_emerg_cleanup();
+
     if (kbd->console_fd >= 0) {
         /* Restore the original keyboard mode */
         ioctl(kbd->console_fd, KDSKBMODE, kbd->old_kbd_mode);