Don't log stuff while holding the GIL!
[collectd.git] / src / python.c
index f9360c3..3b227c9 100644 (file)
@@ -1,6 +1,7 @@
 #include <Python.h>
 #include <structmember.h>
 
+#include <signal.h>
 #if HAVE_PTHREAD_H
 # include <pthread.h>
 #endif
@@ -17,6 +18,173 @@ typedef struct cpy_callback_s {
        struct cpy_callback_s *next;
 } cpy_callback_t;
 
+static char log_doc[] = "This function sends a string to all logging plugins.";
+
+static char flush_doc[] = "flush([plugin][, timeout][, identifier]) -> None\n"
+               "\n"
+               "Flushes the cache of another plugin.";
+
+static char unregister_doc[] = "Unregisters a callback. This function needs exactly one parameter either\n"
+               "the function to unregister or the callback identifier to unregister.";
+
+static char reg_log_doc[] = "register_log(callback[, data][, name]) -> identifier\n"
+               "\n"
+               "Register a callback function for log messages.\n"
+               "\n"
+               "'callback' is a callable object that will be called every time something\n"
+               "    is logged.\n"
+               "'data' is an optional object that will be passed back to the callback\n"
+               "    function every time it is called.\n"
+               "'name' is an optional identifier for this callback. The default name\n"
+               "    is 'python.<module>.<name>'. If 'name' contains a '.' it\n"
+               "    replaces both module and name, otherwise it replaces only name.\n"
+               "    Every callback needs a unique identifier, so if you want to\n"
+               "    register one function multiple time you need to specify a name\n"
+               "    here.\n"
+               "'identifier' is the full identifier assigned to this callback.\n"
+               "\n"
+               "The callback function will be called with two or three parameters:\n"
+               "severity: An integer that should be compared to the LOG_ constants.\n"
+               "message: The text to be logged.\n"
+               "data: The optional data parameter passed to the register function.\n"
+               "    If the parameter was obmitted it will be obmitted here, too.";
+
+static char reg_init_doc[] = "register_init(callback[, data][, name]) -> identifier\n"
+               "\n"
+               "Register a callback function that will be executed once after the config.\n"
+               "file has been read, all plugins heve been loaded and the collectd has\n"
+               "forked into the backgroud.\n"
+               "\n"
+               "'callback' is a callable object that will be executed.\n"
+               "'data' is an optional object that will be passed back to the callback\n"
+               "    function when it is called.\n"
+               "'name' is an optional identifier for this callback. The default name\n"
+               "    is 'python.<module>.<name>'. If 'name' contains a '.' it\n"
+               "    replaces both module and name, otherwise it replaces only name.\n"
+               "    Every callback needs a unique identifier, so if you want to\n"
+               "    register one function multiple time you need to specify a name\n"
+               "    here.\n"
+               "'identifier' is the full identifier assigned to this callback.\n"
+               "\n"
+               "The callback function will be called without parameters, except for\n"
+               "data if it was supplied.";
+
+static char reg_config_doc[] = "register_config(callback[, data][, name]) -> identifier\n"
+               "\n"
+               "Register a callback function for config file entries.\n"
+               "'callback' is a callable object that will be called for every config block.\n"
+               "'data' is an optional object that will be passed back to the callback\n"
+               "    function every time it is called.\n"
+               "'name' is an optional identifier for this callback. The default name\n"
+               "    is 'python.<module>'. Every callback needs a unique identifier,\n"
+               "    so if you want to register one function multiple time you need to\n"
+               "    specify a name here.\n"
+               "'identifier' is the full identifier assigned to this callback.\n"
+               "\n"
+               "The callback function will be called with one or two parameters:\n"
+               "config: A Config object.\n"
+               "data: The optional data parameter passed to the register function.\n"
+               "    If the parameter was obmitted it will be obmitted here, too.";
+
+static char reg_read_doc[] = "register_read(callback[, interval][, data][, name]) -> identifier\n"
+               "\n"
+               "Register a callback function for reading data. It will just be called\n"
+               "in a fixed interval to signal that it's time to dispatch new values.\n"
+               "'callback' is a callable object that will be called every time something\n"
+               "    is logged.\n"
+               "'interval' is the number of seconds between between calls to the callback\n"
+               "    function. Full float precision is supported here.\n"
+               "'data' is an optional object that will be passed back to the callback\n"
+               "    function every time it is called.\n"
+               "'name' is an optional identifier for this callback. The default name\n"
+               "    is 'python.<module>.<name>'. If 'name' contains a '.' it\n"
+               "    replaces both module and name, otherwise it replaces only name.\n"
+               "    Every callback needs a unique identifier, so if you want to\n"
+               "    register one function multiple time you need to specify a name\n"
+               "    here.\n"
+               "'identifier' is the full identifier assigned to this callback.\n"
+               "\n"
+               "The callback function will be called without parameters, except for\n"
+               "data if it was supplied.";
+
+static char reg_write_doc[] = "register_write(callback[, data][, name]) -> identifier\n"
+               "\n"
+               "Register a callback function to receive values dispatched by other plugins.\n"
+               "'callback' is a callable object that will be called every time a value\n"
+               "    is dispatched.\n"
+               "'data' is an optional object that will be passed back to the callback\n"
+               "    function every time it is called.\n"
+               "'name' is an optional identifier for this callback. The default name\n"
+               "    is 'python.<module>.<name>'. If 'name' contains a '.' it\n"
+               "    replaces both module and name, otherwise it replaces only name.\n"
+               "    Every callback needs a unique identifier, so if you want to\n"
+               "    register one function multiple time you need to specify a name\n"
+               "    here.\n"
+               "'identifier' is the full identifier assigned to this callback.\n"
+               "\n"
+               "The callback function will be called with one or two parameters:\n"
+               "values: A Values object which is a copy of the dispatched values.\n"
+               "data: The optional data parameter passed to the register function.\n"
+               "    If the parameter was obmitted it will be obmitted here, too.";
+
+static char reg_notification_doc[] = "register_notification(callback[, data][, name]) -> identifier\n"
+               "\n"
+               "Register a callback function for notifications.\n"
+               "'callback' is a callable object that will be called every time a notification\n"
+               "    is dispatched.\n"
+               "'data' is an optional object that will be passed back to the callback\n"
+               "    function every time it is called.\n"
+               "'name' is an optional identifier for this callback. The default name\n"
+               "    is 'python.<module>.<name>'. If 'name' contains a '.' it\n"
+               "    replaces both module and name, otherwise it replaces only name.\n"
+               "    Every callback needs a unique identifier, so if you want to\n"
+               "    register one function multiple time you need to specify a name\n"
+               "    here.\n"
+               "'identifier' is the full identifier assigned to this callback.\n"
+               "\n"
+               "The callback function will be called with one or two parameters:\n"
+               "notification: A copy of the notification that was dispatched.\n"
+               "data: The optional data parameter passed to the register function.\n"
+               "    If the parameter was obmitted it will be obmitted here, too.";
+
+static char reg_flush_doc[] = "register_flush(callback[, data][, name]) -> identifier\n"
+               "\n"
+               "Register a callback function for flush messages.\n"
+               "'callback' is a callable object that will be called every time a plugin\n"
+               "    requests a flush for either this or all plugins.\n"
+               "'data' is an optional object that will be passed back to the callback\n"
+               "    function every time it is called.\n"
+               "'name' is an optional identifier for this callback. The default name\n"
+               "    is 'python.<module>'. Every callback needs a unique identifier,\n"
+               "    so if you want to register one function multiple time you need to\n"
+               "    specify a name here.\n"
+               "'identifier' is the full identifier assigned to this callback.\n"
+               "\n"
+               "The callback function will be called with two or three parameters:\n"
+               "timeout: ???.\n"
+               "id: ???.\n"
+               "data: The optional data parameter passed to the register function.\n"
+               "    If the parameter was obmitted it will be obmitted here, too.";
+
+static char reg_shutdown_doc[] = "register_shutdown(callback[, data][, name]) -> identifier\n"
+               "\n"
+               "Register a callback function for collectd shutdown.\n"
+               "'callback' is a callable object that will be called once collectd is\n"
+               "    shutting down.\n"
+               "'data' is an optional object that will be passed back to the callback\n"
+               "    function if it is called.\n"
+               "'name' is an optional identifier for this callback. The default name\n"
+               "    is 'python.<module>.<name>'. If 'name' contains a '.' it\n"
+               "    replaces both module and name, otherwise it replaces only name.\n"
+               "    Every callback needs a unique identifier, so if you want to\n"
+               "    register one function multiple time you need to specify a name\n"
+               "    here.\n"
+               "'identifier' is the full identifier assigned to this callback.\n"
+               "\n"
+               "The callback function will be called with no parameters except for\n"
+               "    data if it was supplied.";
+
+
 static int do_interactive = 0;
 
 /* This is our global thread state. Python saves some stuff in thread-local
@@ -100,7 +268,9 @@ static void cpy_log_exception(const char *context) {
                typename = "NamelessException";
        if (message == NULL)
                message = "N/A";
+       Py_BEGIN_ALLOW_THREADS
        ERROR("Unhandled python exception in %s: %s: %s", context, typename, message);
+       Py_END_ALLOW_THREADS
        Py_XDECREF(tn);
        Py_XDECREF(m);
        if (!cpy_format_exception) {
@@ -114,21 +284,24 @@ static void cpy_log_exception(const char *context) {
                PyErr_Clear();
                return;
        }
-       list = PyObject_CallFunction(cpy_format_exception, "NNN", type, value, traceback);
+       list = PyObject_CallFunction(cpy_format_exception, "NNN", type, value, traceback); /* New reference. */
        if (list)
                l = PyObject_Length(list);
        for (i = 0; i < l; ++i) {
                char *s;
                PyObject *line;
                
-               line = PyList_GET_ITEM(list, i);
+               line = PyList_GET_ITEM(list, i); /* Borrowed reference. */
                s = strdup(PyString_AsString(line));
                Py_DECREF(line);
                if (s[strlen(s) - 1] == '\n')
                        s[strlen(s) - 1] = 0;
+               Py_BEGIN_ALLOW_THREADS
                ERROR("%s", s);
+               Py_END_ALLOW_THREADS
                free(s);
        }
+       Py_XDECREF(list);
        PyErr_Clear();
 }
 
@@ -144,6 +317,8 @@ static int cpy_read_callback(user_data_t *data) {
                        Py_DECREF(ret);
                }
        CPY_RELEASE_THREADS
+       if (ret == NULL)
+               return 1;
        return 0;
 }
 
@@ -177,7 +352,9 @@ static int cpy_write_callback(const data_set_t *ds, const value_list_t *value_li
                                else
                                        PyList_SetItem(list, i, PyLong_FromUnsignedLongLong(value_list->values[i].absolute));
                        } else {
+                               Py_BEGIN_ALLOW_THREADS
                                ERROR("cpy_write_callback: Unknown value type %d.", ds->ds->type);
+                               Py_END_ALLOW_THREADS
                                Py_DECREF(list);
                                CPY_RETURN_FROM_THREADS 0;
                        }
@@ -200,6 +377,24 @@ static int cpy_write_callback(const data_set_t *ds, const value_list_t *value_li
        return 0;
 }
 
+static int cpy_notification_callback(const notification_t *notification, user_data_t *data) {
+       cpy_callback_t *c = data->data;
+       PyObject *ret, *n;
+
+       CPY_LOCK_THREADS
+               n = PyObject_CallFunction((PyObject *) &NotificationType, "ssssssdi", notification->type, notification->message,
+                               notification->plugin_instance, notification->type_instance, notification->plugin,
+                               notification->host, (double) notification->time, notification->severity);
+               ret = PyObject_CallFunctionObjArgs(c->callback, n, c->data, (void *) 0); /* New reference. */
+               if (ret == NULL) {
+                       cpy_log_exception("notification callback");
+               } else {
+                       Py_DECREF(ret);
+               }
+       CPY_RELEASE_THREADS
+       return 0;
+}
+
 static void cpy_log_callback(int severity, const char *message, user_data_t *data) {
        cpy_callback_t * c = data->data;
        PyObject *ret;
@@ -358,6 +553,10 @@ static PyObject *cpy_register_write(PyObject *self, PyObject *args, PyObject *kw
        return cpy_register_generic_userdata(plugin_register_write, cpy_write_callback, args, kwds, 0);
 }
 
+static PyObject *cpy_register_notification(PyObject *self, PyObject *args, PyObject *kwds) {
+       return cpy_register_generic_userdata(plugin_register_notification, cpy_notification_callback, args, kwds, 0);
+}
+
 static PyObject *cpy_register_flush(PyObject *self, PyObject *args, PyObject *kwds) {
        return cpy_register_generic_userdata(plugin_register_flush, cpy_flush_callback, args, kwds, 1);
 }
@@ -406,7 +605,9 @@ static PyObject *cpy_debug(PyObject *self, PyObject *args) {
 #ifdef COLLECT_DEBUG
        const char *text;
        if (PyArg_ParseTuple(args, "s", &text) == 0) return NULL;
+       Py_BEGIN_ALLOW_THREADS
        plugin_log(LOG_DEBUG, "%s", text);
+       Py_END_ALLOW_THREADS
 #endif
        Py_RETURN_NONE;
 }
@@ -486,6 +687,10 @@ static PyObject *cpy_unregister_write(PyObject *self, PyObject *arg) {
        return cpy_unregister_generic_userdata(plugin_unregister_write, arg, "write", 0);
 }
 
+static PyObject *cpy_unregister_notification(PyObject *self, PyObject *arg) {
+       return cpy_unregister_generic_userdata(plugin_unregister_notification, arg, "notification", 0);
+}
+
 static PyObject *cpy_unregister_flush(PyObject *self, PyObject *arg) {
        return cpy_unregister_generic_userdata(plugin_unregister_flush, arg, "flush", 1);
 }
@@ -495,26 +700,28 @@ static PyObject *cpy_unregister_shutdown(PyObject *self, PyObject *arg) {
 }
 
 static PyMethodDef cpy_methods[] = {
-       {"debug", cpy_debug, METH_VARARGS, "This is an unhelpful text."},
-       {"info", cpy_info, METH_VARARGS, "This is an unhelpful text."},
-       {"notice", cpy_notice, METH_VARARGS, "This is an unhelpful text."},
-       {"warning", cpy_warning, METH_VARARGS, "This is an unhelpful text."},
-       {"error", cpy_error, METH_VARARGS, "This is an unhelpful text."},
-       {"flush", (PyCFunction) cpy_flush, METH_VARARGS | METH_KEYWORDS, "This is an unhelpful text."},
-       {"register_log", (PyCFunction) cpy_register_log, METH_VARARGS | METH_KEYWORDS, "This is an unhelpful text."},
-       {"register_init", (PyCFunction) cpy_register_init, METH_VARARGS | METH_KEYWORDS, "This is an unhelpful text."},
-       {"register_config", (PyCFunction) cpy_register_config, METH_VARARGS | METH_KEYWORDS, "This is an unhelpful text."},
-       {"register_read", (PyCFunction) cpy_register_read, METH_VARARGS | METH_KEYWORDS, "This is an unhelpful text."},
-       {"register_write", (PyCFunction) cpy_register_write, METH_VARARGS | METH_KEYWORDS, "This is an unhelpful text."},
-       {"register_flush", (PyCFunction) cpy_register_flush, METH_VARARGS | METH_KEYWORDS, "This is an unhelpful text."},
-       {"register_shutdown", (PyCFunction) cpy_register_shutdown, METH_VARARGS | METH_KEYWORDS, "This is an unhelpful text."},
-       {"unregister_log", cpy_unregister_log, METH_O, "This is an unhelpful text."},
-       {"unregister_init", cpy_unregister_init, METH_O, "This is an unhelpful text."},
-       {"unregister_config", cpy_unregister_config, METH_O, "This is an unhelpful text."},
-       {"unregister_read", cpy_unregister_read, METH_O, "This is an unhelpful text."},
-       {"unregister_write", cpy_unregister_write, METH_O, "This is an unhelpful text."},
-       {"unregister_flush", cpy_unregister_flush, METH_O, "This is an unhelpful text."},
-       {"unregister_shutdown", cpy_unregister_shutdown, METH_O, "This is an unhelpful text."},
+       {"debug", cpy_debug, METH_VARARGS, log_doc},
+       {"info", cpy_info, METH_VARARGS, log_doc},
+       {"notice", cpy_notice, METH_VARARGS, log_doc},
+       {"warning", cpy_warning, METH_VARARGS, log_doc},
+       {"error", cpy_error, METH_VARARGS, log_doc},
+       {"flush", (PyCFunction) cpy_flush, METH_VARARGS | METH_KEYWORDS, flush_doc},
+       {"register_log", (PyCFunction) cpy_register_log, METH_VARARGS | METH_KEYWORDS, reg_log_doc},
+       {"register_init", (PyCFunction) cpy_register_init, METH_VARARGS | METH_KEYWORDS, reg_init_doc},
+       {"register_config", (PyCFunction) cpy_register_config, METH_VARARGS | METH_KEYWORDS, reg_config_doc},
+       {"register_read", (PyCFunction) cpy_register_read, METH_VARARGS | METH_KEYWORDS, reg_read_doc},
+       {"register_write", (PyCFunction) cpy_register_write, METH_VARARGS | METH_KEYWORDS, reg_write_doc},
+       {"register_notification", (PyCFunction) cpy_register_notification, METH_VARARGS | METH_KEYWORDS, reg_notification_doc},
+       {"register_flush", (PyCFunction) cpy_register_flush, METH_VARARGS | METH_KEYWORDS, reg_flush_doc},
+       {"register_shutdown", (PyCFunction) cpy_register_shutdown, METH_VARARGS | METH_KEYWORDS, reg_shutdown_doc},
+       {"unregister_log", cpy_unregister_log, METH_O, unregister_doc},
+       {"unregister_init", cpy_unregister_init, METH_O, unregister_doc},
+       {"unregister_config", cpy_unregister_config, METH_O, unregister_doc},
+       {"unregister_read", cpy_unregister_read, METH_O, unregister_doc},
+       {"unregister_write", cpy_unregister_write, METH_O, unregister_doc},
+       {"unregister_notification", cpy_unregister_notification, METH_O, unregister_doc},
+       {"unregister_flush", cpy_unregister_flush, METH_O, unregister_doc},
+       {"unregister_shutdown", cpy_unregister_shutdown, METH_O, unregister_doc},
        {0, 0, 0, 0}
 };
 
@@ -533,20 +740,59 @@ static int cpy_shutdown(void) {
                else
                        Py_DECREF(ret);
        }
+       PyErr_Print();
        Py_Finalize();
        return 0;
 }
 
+static void cpy_int_handler(int sig) {
+       return;
+}
+
 static void *cpy_interactive(void *data) {
-       CPY_LOCK_THREADS
-               if (PyImport_ImportModule("readline") == NULL) {
-                       /* This interactive session will suck. */
-                       cpy_log_exception("interactive session init");
-               }
-               PyRun_InteractiveLoop(stdin, "<stdin>");
-       CPY_RELEASE_THREADS
+       sigset_t sigset;
+       struct sigaction sig_int_action, old;
+       
+       /* Signal handler in a plugin? Bad stuff, but the best way to
+        * handle it I guess. In an interactive session people will
+        * press Ctrl+C at some time, which will generate a SIGINT.
+        * This will cause collectd to shutdown, thus killing the
+        * interactive interpreter, and leaving the terminal in a
+        * 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.
+        *
+        * 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. */
+       memset (&sig_int_action, '\0', sizeof (sig_int_action));
+       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);
+       PyEval_AcquireThread(state);
+       if (PyImport_ImportModule("readline") == NULL) {
+               /* This interactive session will suck. */
+               cpy_log_exception("interactive session init");
+       }
+       PyRun_InteractiveLoop(stdin, "<stdin>");
+       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();
        return NULL;
 }
 
@@ -554,6 +800,7 @@ static int cpy_init(void) {
        cpy_callback_t *c;
        PyObject *ret;
        static pthread_t thread;
+       sigset_t sigset;
        
        PyEval_InitThreads();
        /* Now it's finally OK to use python threads. */
@@ -564,6 +811,9 @@ static int cpy_init(void) {
                else
                        Py_DECREF(ret);
        }
+       sigemptyset(&sigset);
+       sigaddset(&sigset, SIGINT);
+       pthread_sigmask(SIG_BLOCK, &sigset, NULL);
        state = PyEval_SaveThread();
        if (do_interactive) {
                if (pthread_create(&thread, NULL, cpy_interactive, NULL)) {
@@ -620,7 +870,11 @@ static int cpy_config(oconfig_item_t *ci) {
        Py_Initialize();
        
        PyType_Ready(&ConfigType);
+       PyType_Ready(&PluginDataType);
+       ValuesType.tp_base = &PluginDataType;
        PyType_Ready(&ValuesType);
+       NotificationType.tp_base = &PluginDataType;
+       PyType_Ready(&NotificationType);
        sys = PyImport_ImportModule("sys"); /* New reference. */
        if (sys == NULL) {
                cpy_log_exception("python initialization");
@@ -635,11 +889,15 @@ static int cpy_config(oconfig_item_t *ci) {
        module = Py_InitModule("collectd", cpy_methods); /* Borrowed reference. */
        PyModule_AddObject(module, "Config", (PyObject *) &ConfigType); /* Steals a reference. */
        PyModule_AddObject(module, "Values", (PyObject *) &ValuesType); /* Steals a reference. */
+       PyModule_AddObject(module, "Notification", (PyObject *) &NotificationType); /* Steals a reference. */
        PyModule_AddIntConstant(module, "LOG_DEBUG", LOG_DEBUG);
        PyModule_AddIntConstant(module, "LOG_INFO", LOG_INFO);
        PyModule_AddIntConstant(module, "LOG_NOTICE", LOG_NOTICE);
        PyModule_AddIntConstant(module, "LOG_WARNING", LOG_WARNING);
        PyModule_AddIntConstant(module, "LOG_ERROR", LOG_ERR);
+       PyModule_AddIntConstant(module, "NOTIF_FAILURE", NOTIF_FAILURE);
+       PyModule_AddIntConstant(module, "NOTIF_WARNING", NOTIF_WARNING);
+       PyModule_AddIntConstant(module, "NOTIF_OKAY", NOTIF_OKAY);
        for (i = 0; i < ci->children_num; ++i) {
                oconfig_item_t *item = ci->children + i;
                
@@ -696,7 +954,7 @@ static int cpy_config(oconfig_item_t *ci) {
                        module = PyImport_ImportModule(module_name); /* New reference. */
                        if (module == NULL) {
                                ERROR("python plugin: Error importing module \"%s\".", module_name);
-                               cpy_log_exception("python initialization");
+                               cpy_log_exception("importing module");
                                PyErr_Print();
                        }
                        free(module_name);
@@ -741,6 +999,5 @@ static int cpy_config(oconfig_item_t *ci) {
 void module_register(void) {
        plugin_register_complex_config("python", cpy_config);
        plugin_register_init("python", cpy_init);
-//     plugin_register_read("python", cna_read);
        plugin_register_shutdown("python", cpy_shutdown);
 }