python plugin: Fix SIGINT handling.
[collectd.git] / src / python.c
index f5b8b93..912c18a 100644 (file)
@@ -219,6 +219,9 @@ static char reg_shutdown_doc[] = "register_shutdown(callback[, data][, name]) ->
                "    data if it was supplied.";
 
 
+static pthread_t main_thread;
+static PyOS_sighandler_t python_sigint_handler;
+static _Bool do_interactive = 0;
 static int do_interactive = 0;
 
 /* This is our global thread state. Python saves some stuff in thread-local
@@ -910,13 +913,8 @@ static int cpy_shutdown(void) {
        return 0;
 }
 
-static void cpy_int_handler(int sig) {
-       return;
-}
-
 static void *cpy_interactive(void *data) {
-       sigset_t sigset;
-       struct sigaction old;
+       PyOS_sighandler_t cur_sig;
 
        /* Signal handler in a plugin? Bad stuff, but the best way to
         * handle it I guess. In an interactive session people will
@@ -926,46 +924,40 @@ static void *cpy_interactive(void *data) {
         * mess. Chances are, this isn't what the user wanted to do.
         *
         * So this is the plan:
-        * 1. Block SIGINT in the main thread.
-        * 2. Install our own signal handler that does nothing.
-        * 3. Unblock SIGINT in the interactive thread.
+        * 1. Restore Python's own signal handler
+        * 2. Tell Python we just forked so it will accept this thread
+        *    as the main one. No version of Python will ever handle
+        *    interrupts anywhere but in the main thread.
+        * 3. After the interactive loop is done, restore collectd's
+        *    SIGINT handler.
+        * 4. Raise SIGINT for a clean shutdown. The signal is sent to
+        *    the main thread to ensure it wakes up the main interval
+        *    sleep so that collectd shuts down immediately not in 10
+        *    seconds.
         *
         * This will make sure that SIGINT won't kill collectd but
-        * still interrupt syscalls like sleep and pause.
-        * It does not raise a KeyboardInterrupt exception because so
-        * far nobody managed to figure out how to do that. */
-       struct sigaction sig_int_action = {
-               .sa_handler = cpy_int_handler
-       };
-       sigaction (SIGINT, &sig_int_action, &old);
-
-       sigemptyset(&sigset);
-       sigaddset(&sigset, SIGINT);
-       pthread_sigmask(SIG_UNBLOCK, &sigset, NULL);
+        * still interrupt syscalls like sleep and pause. */
+
        PyEval_AcquireThread(state);
        if (PyImport_ImportModule("readline") == NULL) {
                /* This interactive session will suck. */
                cpy_log_exception("interactive session init");
-       }
+       }
+       cur_sig = PyOS_setsig(SIGINT, python_sigint_handler);
+       /* We totally forked just now. Everyone saw that, right? */
+       PyOS_AfterFork();
        PyRun_InteractiveLoop(stdin, "<stdin>");
+       PyOS_setsig(SIGINT, cur_sig);
        PyErr_Print();
        PyEval_ReleaseThread(state);
        NOTICE("python: Interactive interpreter exited, stopping collectd ...");
-       /* Restore the original collectd SIGINT handler and raise SIGINT.
-        * The main thread still has SIGINT blocked and there's nothing we
-        * can do about that so this thread will handle it. But that's not
-        * important, except that it won't interrupt the main loop and so
-        * it might take a few seconds before collectd really shuts down. */
-       sigaction (SIGINT, &old, NULL);
-       raise(SIGINT);
-       pause();
+       pthread_kill(main_thread, SIGINT);
        return NULL;
 }
 
 static int cpy_init(void) {
        PyObject *ret;
        static pthread_t thread;
-       sigset_t sigset;
 
        if (!Py_IsInitialized()) {
                WARNING("python: Plugin loaded but not configured.");
@@ -981,10 +973,8 @@ static int cpy_init(void) {
                else
                        Py_DECREF(ret);
        }
-       sigemptyset(&sigset);
-       sigaddset(&sigset, SIGINT);
-       pthread_sigmask(SIG_BLOCK, &sigset, NULL);
        state = PyEval_SaveThread();
+       main_thread = pthread_self();
        if (do_interactive) {
                if (plugin_thread_create(&thread, NULL, cpy_interactive, NULL)) {
                        ERROR("python: Error creating thread for interactive interpreter.");
@@ -1040,6 +1030,7 @@ PyMODINIT_FUNC PyInit_collectd(void) {
 #endif
 
 static int cpy_init_python(void) {
+       PyOS_sighandler_t cur_sig;
        PyObject *sys;
        PyObject *module;
 
@@ -1051,7 +1042,10 @@ static int cpy_init_python(void) {
        char *argv = "";
 #endif
 
+       /* Chances are the current signal handler is already SIG_DFL, but let's make sure. */
+       cur_sig = PyOS_setsig(SIGINT, SIG_DFL);
        Py_Initialize();
+       python_sigint_handler = PyOS_setsig(SIGINT, cur_sig);
 
        PyType_Ready(&ConfigType);
        PyType_Ready(&PluginDataType);
@@ -1104,6 +1098,7 @@ static int cpy_init_python(void) {
 
 static int cpy_config(oconfig_item_t *ci) {
        PyObject *tb;
+       int status = 0;
 
        /* Ok in theory we shouldn't do initialization at this point
         * but we have to. In order to give python scripts a chance
@@ -1118,23 +1113,36 @@ static int cpy_config(oconfig_item_t *ci) {
                oconfig_item_t *item = ci->children + i;
 
                if (strcasecmp(item->key, "Interactive") == 0) {
-                       if (item->values_num != 1 || item->values[0].type != OCONFIG_TYPE_BOOLEAN)
+                       if (cf_util_get_boolean(item, &do_interactive) != 0) {
+                               status = 1;
                                continue;
-                       do_interactive = item->values[0].value.boolean;
+                       }
                } else if (strcasecmp(item->key, "Encoding") == 0) {
-                       if (item->values_num != 1 || item->values[0].type != OCONFIG_TYPE_STRING)
+                       char *encoding = NULL;
+                       if (cf_util_get_string(item, &encoding) != 0) {
+                               status = 1;
                                continue;
+                       }
 #ifdef IS_PY3K
-                       NOTICE("python: \"Encoding\" was used in the config file but Python3 was used, which does not support changing encodings. Ignoring this.");
+                       ERROR("python: \"Encoding\" was used in the config file but Python3 was used, which does not support changing encodings");
+                       status = 1;
+                       sfree(encoding);
+                       continue;
 #else
                        /* Why is this even necessary? And undocumented? */
-                       if (PyUnicode_SetDefaultEncoding(item->values[0].value.string))
+                       if (PyUnicode_SetDefaultEncoding(encoding)) {
                                cpy_log_exception("setting default encoding");
+                               status = 1;
+                       }
 #endif
+                       sfree(encoding);
                } else if (strcasecmp(item->key, "LogTraces") == 0) {
-                       if (item->values_num != 1 || item->values[0].type != OCONFIG_TYPE_BOOLEAN)
+                       _Bool log_traces;
+                       if (cf_util_get_boolean(item, &log_traces) != 0) {
+                               status = 1;
                                continue;
-                       if (!item->values[0].value.boolean) {
+                       }
+                       if (!log_traces) {
                                Py_XDECREF(cpy_format_exception);
                                cpy_format_exception = NULL;
                                continue;
@@ -1144,30 +1152,37 @@ static int cpy_config(oconfig_item_t *ci) {
                        tb = PyImport_ImportModule("traceback"); /* New reference. */
                        if (tb == NULL) {
                                cpy_log_exception("python initialization");
+                               status = 1;
                                continue;
                        }
                        cpy_format_exception = PyObject_GetAttrString(tb, "format_exception"); /* New reference. */
                        Py_DECREF(tb);
-                       if (cpy_format_exception == NULL)
+                       if (cpy_format_exception == NULL) {
                                cpy_log_exception("python initialization");
+                               status = 1;
+                       }
                } else if (strcasecmp(item->key, "ModulePath") == 0) {
                        char *dir = NULL;
                        PyObject *dir_object;
 
-                       if (cf_util_get_string(item, &dir) != 0)
+                       if (cf_util_get_string(item, &dir) != 0) {
+                               status = 1;
                                continue;
+                       }
                        dir_object = cpy_string_to_unicode_or_bytes(dir); /* New reference. */
                        if (dir_object == NULL) {
                                ERROR("python plugin: Unable to convert \"%s\" to "
                                      "a python object.", dir);
                                free(dir);
                                cpy_log_exception("python initialization");
+                               status = 1;
                                continue;
                        }
                        if (PyList_Insert(sys_path, 0, dir_object) != 0) {
                                ERROR("python plugin: Unable to prepend \"%s\" to "
                                      "python module path.", dir);
                                cpy_log_exception("python initialization");
+                               status = 1;
                        }
                        Py_DECREF(dir_object);
                        free(dir);
@@ -1175,12 +1190,15 @@ static int cpy_config(oconfig_item_t *ci) {
                        char *module_name = NULL;
                        PyObject *module;
 
-                       if (cf_util_get_string(item, &module_name) != 0)
+                       if (cf_util_get_string(item, &module_name) != 0) {
+                               status = 1;
                                continue;
+                       }
                        module = PyImport_ImportModule(module_name); /* New reference. */
                        if (module == NULL) {
                                ERROR("python plugin: Error importing module \"%s\".", module_name);
                                cpy_log_exception("importing module");
+                               status = 1;
                        }
                        free(module_name);
                        Py_XDECREF(module);
@@ -1189,8 +1207,10 @@ static int cpy_config(oconfig_item_t *ci) {
                        cpy_callback_t *c;
                        PyObject *ret;
 
-                       if (cf_util_get_string(item, &name) != 0)
+                       if (cf_util_get_string(item, &name) != 0) {
+                               status = 1;
                                continue;
+                       }
                        for (c = cpy_config_callbacks; c; c = c->next) {
                                if (strcasecmp(c->name + 7, name) == 0)
                                        break;
@@ -1209,15 +1229,17 @@ static int cpy_config(oconfig_item_t *ci) {
                        else
                                ret = PyObject_CallFunction(c->callback, "NO",
                                        cpy_oconfig_to_pyconfig(item, NULL), c->data); /* New reference. */
-                       if (ret == NULL)
+                       if (ret == NULL) {
                                cpy_log_exception("loading module");
-                       else
+                               status = 1;
+                       } else
                                Py_DECREF(ret);
                } else {
-                       WARNING("python plugin: Ignoring unknown config key \"%s\".", item->key);
+                       ERROR("python plugin: Unknown config key \"%s\".", item->key);
+                       status = 1;
                }
        }
-       return 0;
+       return (status);
 }
 
 void module_register(void) {