drop usage of asyncio and run everything regularly threaded

This commit is contained in:
Michael Zanetti 2020-07-19 23:18:18 +02:00
parent 64e7d2784c
commit 3436e9b998
11 changed files with 218 additions and 251 deletions

View File

@ -7,6 +7,8 @@
#include <QStringList> #include <QStringList>
#include <QLoggingCategory> #include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcPythonIntegrations)
#pragma GCC diagnostic push #pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Winvalid-offsetof" #pragma GCC diagnostic ignored "-Winvalid-offsetof"
#pragma GCC diagnostic ignored "-Wwrite-strings" #pragma GCC diagnostic ignored "-Wwrite-strings"
@ -16,14 +18,23 @@ typedef struct {
char *category; char *category;
} PyNymeaLoggingHandler; } PyNymeaLoggingHandler;
static int PyNymeaLoggingHandler_init(PyNymeaLoggingHandler */*self*/, PyObject */*args*/, PyObject */*kwds*/) static int PyNymeaLoggingHandler_init(PyNymeaLoggingHandler *self, PyObject *args, PyObject */*kwds*/)
{ {
char *category = nullptr;
if (!PyArg_ParseTuple(args, "s", &category)) {
qCWarning(dcPythonIntegrations()) << "PyNymeaLoggingHandler: Error parsing parameters";
return -1;
}
self->category = (char*)malloc(qstrlen(category));
qstrcpy(self->category, category);
return 0; return 0;
} }
static void PyNymeaLoggingHandler_dealloc(PyNymeaLoggingHandler * self) static void PyNymeaLoggingHandler_dealloc(PyNymeaLoggingHandler * self)
// destruct the object
{ {
free(self->category);
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);
} }
@ -106,5 +117,6 @@ static void registerNymeaLoggingHandler(PyObject *module)
} }
} }
#pragma GCC diagnostic pop
#endif // PYNYMEALOGGINGHANDLER_H #endif // PYNYMEALOGGINGHANDLER_H

View File

@ -0,0 +1,62 @@
#ifndef PYNYMEAMODULE_H
#define PYNYMEAMODULE_H
#include <Python.h>
#include "pynymealogginghandler.h"
#include "pything.h"
#include "pythingdiscoveryinfo.h"
#include "pythingsetupinfo.h"
#include "pyparam.h"
#include "pythingactioninfo.h"
#include "pythingpairinginfo.h"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Winvalid-offsetof"
#pragma GCC diagnostic ignored "-Wwrite-strings"
static int nymea_exec(PyObject *m) {
// Override stdout/stderr to use qDebug instead
PyObject* pyLog = PyModule_Create(&pyLog_module);
PySys_SetObject("stdout", pyLog);
PyObject* pyWarn = PyModule_Create(&pyWarn_module);
PySys_SetObject("stderr", pyWarn);
registerNymeaLoggingHandler(m);
registerParamType(m);
registerThingType(m);
registerThingDescriptorType(m);
registerThingDiscoveryInfoType(m);
registerThingPairingInfoType(m);
registerThingSetupInfoType(m);
registerThingActionInfoType(m);
return 0;
}
static struct PyModuleDef_Slot nymea_slots[] = {
{Py_mod_exec, (void*)nymea_exec},
{0, NULL},
};
static struct PyModuleDef nymea_module = {
PyModuleDef_HEAD_INIT,
"nymea",
"The nymea module. Provdes types used in the nymea plugin API.",
0,
nullptr, // methods
nymea_slots, // slots
nullptr,
nullptr,
nullptr
};
PyMODINIT_FUNC PyInit_nymea(void)
{
return PyModuleDef_Init(&nymea_module);
}
#pragma GCC diagnostic pop
#endif // PYNYMEAMODULE_H

View File

@ -35,7 +35,7 @@ static PyMemberDef PyParam_members[] = {
static int PyParam_init(PyParam *self, PyObject *args, PyObject *kwds) static int PyParam_init(PyParam *self, PyObject *args, PyObject *kwds)
{ {
qWarning() << "++++ PyParam"; qCDebug(dcPythonIntegrations()) << "+++ PyParam";
static char *kwlist[] = {"paramTypeId", "value", nullptr}; static char *kwlist[] = {"paramTypeId", "value", nullptr};
PyObject *paramTypeId = nullptr, *value = nullptr; PyObject *paramTypeId = nullptr, *value = nullptr;
@ -54,7 +54,7 @@ static int PyParam_init(PyParam *self, PyObject *args, PyObject *kwds)
} }
static void PyParam_dealloc(PyParam * self) { static void PyParam_dealloc(PyParam * self) {
qWarning() << "---- PyParam"; qCDebug(dcPythonIntegrations()) << "--- PyParam";
Py_XDECREF(self->pyParamTypeId); Py_XDECREF(self->pyParamTypeId);
Py_XDECREF(self->pyValue); Py_XDECREF(self->pyValue);
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);

View File

@ -53,7 +53,7 @@ static PyObject* PyThing_new(PyTypeObject *type, PyObject */*args*/, PyObject */
if (self == NULL) { if (self == NULL) {
return nullptr; return nullptr;
} }
qWarning() << "*++++ PyThing" << self; qCDebug(dcPythonIntegrations()) << "+++ PyThing" << self;
return (PyObject*)self; return (PyObject*)self;
} }
@ -127,7 +127,7 @@ static void PyThing_setThing(PyThing *self, Thing *thing)
static void PyThing_dealloc(PyThing * self) { static void PyThing_dealloc(PyThing * self) {
qWarning() << "----- PyThing" << self; qCDebug(dcPythonIntegrations()) << "--- PyThing" << self;
Py_XDECREF(self->pyId); Py_XDECREF(self->pyId);
Py_XDECREF(self->pyThingClassId); Py_XDECREF(self->pyThingClassId);
Py_XDECREF(self->pyName); Py_XDECREF(self->pyName);

View File

@ -43,7 +43,7 @@ static PyObject* PyThingActionInfo_new(PyTypeObject *type, PyObject */*args*/, P
if (self == NULL) { if (self == NULL) {
return nullptr; return nullptr;
} }
qWarning() << "+++ PyThingActionInfo"; qCDebug(dcPythonIntegrations()) << "+++ PyThingActionInfo";
return (PyObject*)self; return (PyObject*)self;
} }
@ -59,7 +59,7 @@ void PyThingActionInfo_setInfo(PyThingActionInfo *self, ThingActionInfo *info, P
static void PyThingActionInfo_dealloc(PyThingActionInfo * self) static void PyThingActionInfo_dealloc(PyThingActionInfo * self)
{ {
qWarning() << "---- PyThingActionInfo"; qCDebug(dcPythonIntegrations()) << "--- PyThingActionInfo";
Py_DECREF(self->pyThing); Py_DECREF(self->pyThing);
Py_DECREF(self->pyActionTypeId); Py_DECREF(self->pyActionTypeId);
Py_DECREF(self->pyParams); Py_DECREF(self->pyParams);

View File

@ -41,7 +41,7 @@ static PyObject* PyThingSetupInfo_new(PyTypeObject *type, PyObject *args, PyObje
if (self == NULL) { if (self == NULL) {
return nullptr; return nullptr;
} }
qWarning() << "++++ PyThingSetupInfo"; qCDebug(dcPythonIntegrations()) << "+++ PyThingSetupInfo";
static char *kwlist[] = {"thing", nullptr}; static char *kwlist[] = {"thing", nullptr};
@ -60,7 +60,7 @@ static PyObject* PyThingSetupInfo_new(PyTypeObject *type, PyObject *args, PyObje
} }
static void PyThingSetupInfo_dealloc(PyThingSetupInfo * self) { static void PyThingSetupInfo_dealloc(PyThingSetupInfo * self) {
qWarning() << "--- PyThingSetupInfo"; qCDebug(dcPythonIntegrations()) << "--- PyThingSetupInfo";
Py_DECREF(self->pyThing); Py_DECREF(self->pyThing);
Py_TYPE(self)->tp_free(self); Py_TYPE(self)->tp_free(self);
} }

View File

@ -65,7 +65,7 @@ QVariant PyObjectToQVariant(PyObject *pyObject)
return QVariant(PyObject_IsTrue(pyObject)); return QVariant(PyObject_IsTrue(pyObject));
} }
Q_ASSERT_X(false, "pyutils.h", "Unhandled data type in conversion PyObject to QVariant!"); Q_ASSERT_X(false, "pyutils.h", QString("Unhandled data type in conversion PyObject to QVariant: %1").arg(pyObject->ob_type->tp_name).toUtf8());
return QVariant(); return QVariant();
} }

View File

@ -1,14 +1,7 @@
#include <Python.h> #include <Python.h>
#include "python/pynymealogginghandler.h"
#include "python/pything.h"
#include "python/pythingdiscoveryinfo.h"
#include "python/pythingsetupinfo.h"
#include "python/pyparam.h"
#include "python/pythingactioninfo.h"
#include "python/pythingpairinginfo.h"
#include "pythonintegrationplugin.h" #include "pythonintegrationplugin.h"
#include "python/pynymeamodule.h"
#include "loggingcategories.h" #include "loggingcategories.h"
@ -22,82 +15,8 @@
#include <QFutureWatcher> #include <QFutureWatcher>
PyThreadState* PythonIntegrationPlugin::s_mainThreadState = nullptr; PyThreadState* PythonIntegrationPlugin::s_mainThreadState = nullptr;
QThreadPool* PythonIntegrationPlugin::s_threadPool = nullptr;
QHash<PythonIntegrationPlugin*, PyObject*> PythonIntegrationPlugin::s_plugins; QHash<PythonIntegrationPlugin*, PyObject*> PythonIntegrationPlugin::s_plugins;
PyObject* PythonIntegrationPlugin::task_done(PyObject* self, PyObject* args)
{
Q_UNUSED(self)
PyObject *result = nullptr;
if (!PyArg_ParseTuple(args, "O", &result)) {
qCWarning(dcPythonIntegrations()) << "Cannot fetch result from coroutine callback.";
return nullptr;
}
PyObject *exceptionMethod = PyObject_GetAttrString(result, "exception");
PyObject *exception = PyObject_CallFunctionObjArgs(exceptionMethod, nullptr);
if (exception != Py_None) {
PyObject* repr = PyObject_Repr(exception);
PyObject* str = PyUnicode_AsEncodedString(repr, "utf-8", "~E~");
const char *bytes = PyBytes_AS_STRING(str);
Py_XDECREF(repr);
Py_XDECREF(str);
// PyObject *traceback = PyObject_CallMethodObjArgs(exception, "__traceback__", nullptr);
qCWarning(dcPythonIntegrations()) << "Exception in plugin:" << bytes;
PyErr_Clear();
}
Py_RETURN_NONE;
}
static PyMethodDef nymea_methods[] =
{
{"task_done", PythonIntegrationPlugin::task_done, METH_VARARGS, "callback to clean up after asyc coroutines"},
{nullptr, nullptr, 0, nullptr} // sentinel
};
static PyModuleDef nymea_module =
{
PyModuleDef_HEAD_INIT, // PyModuleDef_Base m_base;
"nymea", // const char* m_name;
"nymea module for python based integration plugins", // const char* m_doc;
-1, // Py_ssize_t m_size;
nymea_methods, // PyMethodDef *m_methods
nullptr, nullptr, nullptr, nullptr
};
PyMODINIT_FUNC PyInit_nymea(void)
{
// Overrride stdout/stderr to use qDebug instead
PyObject* pyLog = PyModule_Create(&pyLog_module);
PySys_SetObject("stdout", pyLog);
PyObject* pyWarn = PyModule_Create(&pyWarn_module);
PySys_SetObject("stderr", pyWarn);
// Register nymea types
PyObject* m = PyModule_Create(&nymea_module);
registerNymeaLoggingHandler(m);
registerParamType(m);
registerThingType(m);
registerThingDescriptorType(m);
registerThingDiscoveryInfoType(m);
registerThingPairingInfoType(m);
registerThingSetupInfoType(m);
registerThingActionInfoType(m);
return m;
}
PyObject* PythonIntegrationPlugin::pyConfiguration(PyObject* self, PyObject* /*args*/) PyObject* PythonIntegrationPlugin::pyConfiguration(PyObject* self, PyObject* /*args*/)
{ {
PythonIntegrationPlugin *plugin = s_plugins.key(self); PythonIntegrationPlugin *plugin = s_plugins.key(self);
@ -274,17 +193,27 @@ PythonIntegrationPlugin::PythonIntegrationPlugin(QObject *parent) : IntegrationP
PythonIntegrationPlugin::~PythonIntegrationPlugin() PythonIntegrationPlugin::~PythonIntegrationPlugin()
{ {
if (m_pluginModule) {
callPluginFunction("deinit");
}
// Acquire GIL for this plugin's interpreter // Acquire GIL for this plugin's interpreter
PyEval_RestoreThread(m_threadState); PyEval_RestoreThread(m_threadState);
// Cancel all the thread in here
while (!m_runningTasks.isEmpty()) { while (!m_runningTasks.isEmpty()) {
QFutureWatcher<void> *watcher = m_runningTasks.values().first(); QFutureWatcher<void> *watcher = m_runningTasks.keys().first();
watcher->cancel(); QString function = m_runningTasks.value(watcher);
Py_BEGIN_ALLOW_THREADS
qCDebug(dcPythonIntegrations()) << "Waiting for" << metadata().pluginName() << "to finish" << function;
watcher->waitForFinished(); watcher->waitForFinished();
Py_END_ALLOW_THREADS
} }
Py_XDECREF(s_plugins.take(this)); s_plugins.take(this);
Py_XDECREF(m_pluginModule);
Py_XDECREF(m_logger);
Py_DECREF(m_nymeaModule);
Py_EndInterpreter(m_threadState); Py_EndInterpreter(m_threadState);
@ -301,33 +230,25 @@ void PythonIntegrationPlugin::initPython()
PyEval_InitThreads(); PyEval_InitThreads();
// Store the main thread state and release the GIL // Store the main thread state and release the GIL
s_mainThreadState = PyEval_SaveThread(); s_mainThreadState = PyEval_SaveThread();
// Allocate a shared thread pool for the plugins
s_threadPool = new QThreadPool();
qCDebug(dcPythonIntegrations()) << "Created a thread pool with a maximum of" << s_threadPool->maxThreadCount() << "threads for python plugins.";
} }
void PythonIntegrationPlugin::deinitPython() void PythonIntegrationPlugin::deinitPython()
{ {
PyEval_RestoreThread(s_mainThreadState); PyEval_RestoreThread(s_mainThreadState);
Py_FinalizeEx(); Py_FinalizeEx();
s_threadPool->deleteLater();
s_threadPool = nullptr;
} }
bool PythonIntegrationPlugin::loadScript(const QString &scriptFile) bool PythonIntegrationPlugin::loadScript(const QString &scriptFile)
{ {
QFileInfo fi(scriptFile); QFileInfo fi(scriptFile);
QFile metaData(fi.absolutePath() + "/" + fi.baseName() + ".json"); QFile metaDataFile(fi.absolutePath() + "/" + fi.baseName() + ".json");
if (!metaData.open(QFile::ReadOnly)) { if (!metaDataFile.open(QFile::ReadOnly)) {
qCWarning(dcThingManager()) << "Error opening metadata file:" << metaData.fileName(); qCWarning(dcThingManager()) << "Error opening metadata file:" << metaDataFile.fileName();
return false; return false;
} }
QJsonParseError error; QJsonParseError error;
QJsonDocument jsonDoc = QJsonDocument::fromJson(metaData.readAll(), &error); QJsonDocument jsonDoc = QJsonDocument::fromJson(metaDataFile.readAll(), &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCWarning(dcThingManager()) << "Error parsing metadata file:" << error.errorString(); qCWarning(dcThingManager()) << "Error parsing metadata file:" << error.errorString();
return false; return false;
@ -350,19 +271,21 @@ bool PythonIntegrationPlugin::loadScript(const QString &scriptFile)
// Import nymea module into this interpreter // Import nymea module into this interpreter
m_nymeaModule = PyImport_ImportModule("nymea"); m_nymeaModule = PyImport_ImportModule("nymea");
// Set up import paths for the plugin // Set up import path for the plugin directory
PyObject* sysPath = PySys_GetObject("path"); PyObject* sysPath = PySys_GetObject("path");
PyObject* pluginImportPath = PyUnicode_FromString(fi.absolutePath().toUtf8()); PyObject* pluginImportPath = PyUnicode_FromString(fi.absolutePath().toUtf8());
PyList_Append(sysPath, pluginImportPath); PyList_Append(sysPath, pluginImportPath);
Py_DECREF(pluginImportPath); Py_DECREF(pluginImportPath);
// Set up import path for the "modules" subdir in the plugin directory
PyObject* pluginModulesImportPath = PyUnicode_FromString(QString("%1/modules/").arg(fi.absolutePath()).toUtf8()); PyObject* pluginModulesImportPath = PyUnicode_FromString(QString("%1/modules/").arg(fi.absolutePath()).toUtf8());
PyList_Append(sysPath, pluginModulesImportPath); PyList_Append(sysPath, pluginModulesImportPath);
Py_DECREF(pluginModulesImportPath); Py_DECREF(pluginModulesImportPath);
m_module = PyImport_ImportModule(fi.baseName().toUtf8()); // Import the plugin
m_pluginModule = PyImport_ImportModule(fi.baseName().toUtf8());
if (!m_module) { if (!m_pluginModule) {
qCWarning(dcThingManager()) << "Error importing python plugin from:" << fi.absoluteFilePath(); qCWarning(dcThingManager()) << "Error importing python plugin from:" << fi.absoluteFilePath();
PyErr_Print(); PyErr_Print();
PyErr_Clear(); PyErr_Clear();
@ -371,34 +294,37 @@ bool PythonIntegrationPlugin::loadScript(const QString &scriptFile)
} }
qCDebug(dcThingManager()) << "Imported python plugin from" << fi.absoluteFilePath(); qCDebug(dcThingManager()) << "Imported python plugin from" << fi.absoluteFilePath();
s_plugins.insert(this, m_module); s_plugins.insert(this, m_pluginModule);
// We'll be using asyncio everywhere, so let's import it right away
// IMPORTANT: The asyncio module is a bit special in a sense that it actually shares
// stuff between interpreters. See https://docs.python.org/3/c-api/init.html#c.Py_NewInterpreter
// for the listed bugs and caveats.
// If we destroy the first interpreter that imports asyncio, things will become crashy as the
// interpreter will tear down the module, leaving shallow copies of the modules dict in other
// interpreters. So let's import the module down here after we're sure this module compiles and
// won't be unloaded any more.
// Note: This becomes a problem when we'll be supporting to unload plugins at runtime.
m_asyncio = PyImport_ImportModule("asyncio");
// Set up logger with appropriate logging category // Set up logger with appropriate logging category
PyNymeaLoggingHandler *logger = reinterpret_cast<PyNymeaLoggingHandler*>(_PyObject_New(&PyNymeaLoggingHandlerType));
QString category = metadata().pluginName(); QString category = metadata().pluginName();
category.replace(0, 1, category[0].toUpper()); category.replace(0, 1, category[0].toUpper());
logger->category = static_cast<char*>(malloc(category.length() + 1)); PyObject *args = Py_BuildValue("(s)", category.toUtf8().data());
memset(logger->category, '0', category.length() +1); PyNymeaLoggingHandler *logger = reinterpret_cast<PyNymeaLoggingHandler*>(PyObject_CallObject((PyObject*)&PyNymeaLoggingHandlerType, args));
strcpy(logger->category, category.toUtf8().data()); Py_DECREF(args);
PyModule_AddObject(m_module, "logger", reinterpret_cast<PyObject*>(logger)); PyModule_AddObject(m_pluginModule, "logger", reinterpret_cast<PyObject*>(logger));
m_logger = (PyObject*)logger;
// Export metadata ids into module // Export metadata ids into module
exportIds(); exportIds();
// Register config access methods // Register plugin api methods (plugin params etc)
PyModule_AddFunctions(m_module, plugin_methods); PyModule_AddFunctions(m_pluginModule, plugin_methods);
// As python does not have an event loop by default and uses blocking code a lot, we'll
// call every plugin method in a threaded way to prevent blocking the core while still not
// forcing every plugin developer to deal with threading in the plugin.
// In oder to not create and destroy a thread for each plugin api call, we'll be using a
// thread pool.
// The maximum number of threads in a plugin will be amount of things it manages + 2.
// This would allow for e.g. running an event loop using init(), performing something on a thing
// and still allow the user to perform a discovery at the same time. On the other hand, this is
// strict enough to not encourage the plugin developer to block forever in ever api call but use
// proper task processing means (timers, event loops etc) instead.
// Plugins can still spawn more threads on their own if the need to but have to manage them on their own.
m_threadPool = new QThreadPool(this);
m_threadPool->setMaxThreadCount(2);
qCDebug(dcPythonIntegrations()) << "Created a thread pool with a maximum of" << m_threadPool->maxThreadCount() << "threads for python plugin" << metadata().pluginName();
PyEval_ReleaseThread(m_threadState); PyEval_ReleaseThread(m_threadState);
@ -517,18 +443,24 @@ void PythonIntegrationPlugin::setupThing(ThingSetupInfo *info)
PyObject *args = PyTuple_New(1); PyObject *args = PyTuple_New(1);
PyTuple_SetItem(args, 0, (PyObject*)pyThing); PyTuple_SetItem(args, 0, (PyObject*)pyThing);
Py_INCREF(pyThing);
PyThingSetupInfo *pyInfo = (PyThingSetupInfo*)PyObject_CallObject((PyObject*)&PyThingSetupInfoType, args); PyThingSetupInfo *pyInfo = (PyThingSetupInfo*)PyObject_CallObject((PyObject*)&PyThingSetupInfoType, args);
Py_DECREF(args); Py_DECREF(args);
pyInfo->info = info; pyInfo->info = info;
m_threadPool->setMaxThreadCount(m_threadPool->maxThreadCount() + 1);
qCDebug(dcPythonIntegrations()) << "Expanded thread pool for plugin" << metadata().pluginName() << "to" << m_threadPool->maxThreadCount();
PyEval_ReleaseThread(m_threadState); PyEval_ReleaseThread(m_threadState);
connect(info->thing(), &Thing::destroyed, this, [=](){ connect(info->thing(), &Thing::destroyed, this, [=](){
PyEval_RestoreThread(m_threadState); PyEval_RestoreThread(m_threadState);
pyThing->thing = nullptr; pyThing->thing = nullptr;
Py_DECREF(pyThing); Py_DECREF(pyThing);
m_threadPool->setMaxThreadCount(m_threadPool->maxThreadCount() - 1);
qCDebug(dcPythonIntegrations()) << "Shrunk thread pool for plugin" << metadata().pluginName() << "to" << m_threadPool->maxThreadCount();
PyEval_ReleaseThread(m_threadState); PyEval_ReleaseThread(m_threadState);
}); });
connect(info, &ThingSetupInfo::destroyed, this, [=](){ connect(info, &ThingSetupInfo::destroyed, this, [=](){
@ -549,12 +481,7 @@ void PythonIntegrationPlugin::setupThing(ThingSetupInfo *info)
void PythonIntegrationPlugin::postSetupThing(Thing *thing) void PythonIntegrationPlugin::postSetupThing(Thing *thing)
{ {
PyThing* pyThing = m_things.value(thing); PyThing* pyThing = m_things.value(thing);
Py_INCREF(pyThing); callPluginFunction("postSetupThing", reinterpret_cast<PyObject*>(pyThing));
bool success = callPluginFunction("postSetupThing", reinterpret_cast<PyObject*>(pyThing));
if (!success) {
Py_DECREF(pyThing);
}
} }
void PythonIntegrationPlugin::executeAction(ThingActionInfo *info) void PythonIntegrationPlugin::executeAction(ThingActionInfo *info)
@ -569,10 +496,13 @@ void PythonIntegrationPlugin::executeAction(ThingActionInfo *info)
PyEval_ReleaseThread(m_threadState); PyEval_ReleaseThread(m_threadState);
connect(info, &ThingActionInfo::destroyed, this, [=](){ connect(info, &ThingActionInfo::destroyed, this, [=](){
qCDebug(dcPythonIntegrations()) << "Info destroyed";
PyEval_RestoreThread(m_threadState); PyEval_RestoreThread(m_threadState);
qCDebug(dcPythonIntegrations()) << "Info destroyed2";
pyInfo->info = nullptr; pyInfo->info = nullptr;
Py_DECREF(pyInfo); Py_DECREF(pyInfo);
PyEval_ReleaseThread(m_threadState); PyEval_ReleaseThread(m_threadState);
qCDebug(dcPythonIntegrations()) << "Info destroyed3";
}); });
bool success = callPluginFunction("executeAction", reinterpret_cast<PyObject*>(pyInfo)); bool success = callPluginFunction("executeAction", reinterpret_cast<PyObject*>(pyInfo));
@ -584,12 +514,7 @@ void PythonIntegrationPlugin::executeAction(ThingActionInfo *info)
void PythonIntegrationPlugin::thingRemoved(Thing *thing) void PythonIntegrationPlugin::thingRemoved(Thing *thing)
{ {
PyThing *pyThing = m_things.value(thing); PyThing *pyThing = m_things.value(thing);
Py_INCREF(pyThing); callPluginFunction("thingRemoved", reinterpret_cast<PyObject*>(pyThing));
bool success = callPluginFunction("thingRemoved", reinterpret_cast<PyObject*>(pyThing));
if (!success) {
Py_DECREF(pyThing);
}
m_mutex.lock(); m_mutex.lock();
m_things.remove(thing); m_things.remove(thing);
@ -623,13 +548,13 @@ void PythonIntegrationPlugin::exportIds()
QString pluginName = metadata().pluginName(); QString pluginName = metadata().pluginName();
QString pluginId = metadata().pluginId().toString(); QString pluginId = metadata().pluginId().toString();
qCDebug(dcThingManager()) << "- Plugin:" << pluginName << pluginId; qCDebug(dcThingManager()) << "- Plugin:" << pluginName << pluginId;
PyModule_AddStringConstant(m_module, "pluginId", pluginId.toUtf8()); PyModule_AddStringConstant(m_pluginModule, "pluginId", pluginId.toUtf8());
exportParamTypes(configurationDescription(), pluginName, "", "plugin"); exportParamTypes(configurationDescription(), pluginName, "", "plugin");
foreach (const Vendor &vendor, supportedVendors()) { foreach (const Vendor &vendor, supportedVendors()) {
qCDebug(dcThingManager()) << "|- Vendor:" << vendor.name() << vendor.id().toString(); qCDebug(dcThingManager()) << "|- Vendor:" << vendor.name() << vendor.id().toString();
PyModule_AddStringConstant(m_module, QString("%1VendorId").arg(vendor.name()).toUtf8(), vendor.id().toString().toUtf8()); PyModule_AddStringConstant(m_pluginModule, QString("%1VendorId").arg(vendor.name()).toUtf8(), vendor.id().toString().toUtf8());
} }
foreach (const ThingClass &thingClass, supportedThings()) { foreach (const ThingClass &thingClass, supportedThings()) {
@ -642,7 +567,7 @@ void PythonIntegrationPlugin::exportThingClass(const ThingClass &thingClass)
QString variableName = QString("%1ThingClassId").arg(thingClass.name()); QString variableName = QString("%1ThingClassId").arg(thingClass.name());
qCDebug(dcThingManager()) << "|- ThingClass:" << variableName << thingClass.id().toString(); qCDebug(dcThingManager()) << "|- ThingClass:" << variableName << thingClass.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), thingClass.id().toString().toUtf8()); PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), thingClass.id().toString().toUtf8());
exportParamTypes(thingClass.paramTypes(), thingClass.name(), "", "thing"); exportParamTypes(thingClass.paramTypes(), thingClass.name(), "", "thing");
exportParamTypes(thingClass.settingsTypes(), thingClass.name(), "", "settings"); exportParamTypes(thingClass.settingsTypes(), thingClass.name(), "", "settings");
@ -659,7 +584,7 @@ void PythonIntegrationPlugin::exportParamTypes(const ParamTypes &paramTypes, con
foreach (const ParamType &paramType, paramTypes) { foreach (const ParamType &paramType, paramTypes) {
QString variableName = QString("%1ParamTypeId").arg(thingClassName + typeName[0].toUpper() + typeName.right(typeName.length()-1) + typeClass + paramType.name()[0].toUpper() + paramType.name().right(paramType.name().length() -1 )); QString variableName = QString("%1ParamTypeId").arg(thingClassName + typeName[0].toUpper() + typeName.right(typeName.length()-1) + typeClass + paramType.name()[0].toUpper() + paramType.name().right(paramType.name().length() -1 ));
qCDebug(dcThingManager()) << " |- ParamType:" << variableName << paramType.id().toString(); qCDebug(dcThingManager()) << " |- ParamType:" << variableName << paramType.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), paramType.id().toString().toUtf8()); PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), paramType.id().toString().toUtf8());
} }
} }
@ -668,7 +593,7 @@ void PythonIntegrationPlugin::exportStateTypes(const StateTypes &stateTypes, con
foreach (const StateType &stateType, stateTypes) { foreach (const StateType &stateType, stateTypes) {
QString variableName = QString("%1%2StateTypeId").arg(thingClassName, stateType.name()[0].toUpper() + stateType.name().right(stateType.name().length() - 1)); QString variableName = QString("%1%2StateTypeId").arg(thingClassName, stateType.name()[0].toUpper() + stateType.name().right(stateType.name().length() - 1));
qCDebug(dcThingManager()) << " |- StateType:" << variableName << stateType.id().toString(); qCDebug(dcThingManager()) << " |- StateType:" << variableName << stateType.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), stateType.id().toString().toUtf8()); PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), stateType.id().toString().toUtf8());
} }
} }
@ -677,10 +602,9 @@ void PythonIntegrationPlugin::exportEventTypes(const EventTypes &eventTypes, con
foreach (const EventType &eventType, eventTypes) { foreach (const EventType &eventType, eventTypes) {
QString variableName = QString("%1%2EventTypeId").arg(thingClassName, eventType.name()[0].toUpper() + eventType.name().right(eventType.name().length() - 1)); QString variableName = QString("%1%2EventTypeId").arg(thingClassName, eventType.name()[0].toUpper() + eventType.name().right(eventType.name().length() - 1));
qCDebug(dcThingManager()) << " |- EventType:" << variableName << eventType.id().toString(); qCDebug(dcThingManager()) << " |- EventType:" << variableName << eventType.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), eventType.id().toString().toUtf8()); PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), eventType.id().toString().toUtf8());
exportParamTypes(eventType.paramTypes(), thingClassName, "Event", eventType.name()); exportParamTypes(eventType.paramTypes(), thingClassName, "Event", eventType.name());
} }
} }
void PythonIntegrationPlugin::exportActionTypes(const ActionTypes &actionTypes, const QString &thingClassName) void PythonIntegrationPlugin::exportActionTypes(const ActionTypes &actionTypes, const QString &thingClassName)
@ -688,7 +612,7 @@ void PythonIntegrationPlugin::exportActionTypes(const ActionTypes &actionTypes,
foreach (const ActionType &actionType, actionTypes) { foreach (const ActionType &actionType, actionTypes) {
QString variableName = QString("%1%2ActionTypeId").arg(thingClassName, actionType.name()[0].toUpper() + actionType.name().right(actionType.name().length() - 1)); QString variableName = QString("%1%2ActionTypeId").arg(thingClassName, actionType.name()[0].toUpper() + actionType.name().right(actionType.name().length() - 1));
qCDebug(dcThingManager()) << " |- ActionType:" << variableName << actionType.id().toString(); qCDebug(dcThingManager()) << " |- ActionType:" << variableName << actionType.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), actionType.id().toString().toUtf8()); PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), actionType.id().toString().toUtf8());
exportParamTypes(actionType.paramTypes(), thingClassName, "Action", actionType.name()); exportParamTypes(actionType.paramTypes(), thingClassName, "Action", actionType.name());
} }
} }
@ -698,19 +622,17 @@ void PythonIntegrationPlugin::exportBrowserItemActionTypes(const ActionTypes &ac
foreach (const ActionType &actionType, actionTypes) { foreach (const ActionType &actionType, actionTypes) {
QString variableName = QString("%1%2BrowserItemActionTypeId").arg(thingClassName, actionType.name()[0].toUpper() + actionType.name().right(actionType.name().length() - 1)); QString variableName = QString("%1%2BrowserItemActionTypeId").arg(thingClassName, actionType.name()[0].toUpper() + actionType.name().right(actionType.name().length() - 1));
qCDebug(dcThingManager()) << " |- BrowserActionType:" << variableName << actionType.id().toString(); qCDebug(dcThingManager()) << " |- BrowserActionType:" << variableName << actionType.id().toString();
PyModule_AddStringConstant(m_module, variableName.toUtf8(), actionType.id().toString().toUtf8()); PyModule_AddStringConstant(m_pluginModule, variableName.toUtf8(), actionType.id().toString().toUtf8());
exportParamTypes(actionType.paramTypes(), thingClassName, "BrowserItemAction", actionType.name()); exportParamTypes(actionType.paramTypes(), thingClassName, "BrowserItemAction", actionType.name());
} }
} }
bool PythonIntegrationPlugin::callPluginFunction(const QString &function, PyObject *param1, PyObject *param2, PyObject *param3) bool PythonIntegrationPlugin::callPluginFunction(const QString &function, PyObject *param1, PyObject *param2, PyObject *param3)
{ {
PyEval_RestoreThread(m_threadState); PyEval_RestoreThread(m_threadState);
qCDebug(dcThingManager()) << "Calling python plugin function" << function << "on plugin" << pluginName(); qCDebug(dcThingManager()) << "Calling python plugin function" << function << "on plugin" << pluginName();
PyObject *pluginFunction = PyObject_GetAttrString(m_module, function.toUtf8()); PyObject *pluginFunction = PyObject_GetAttrString(m_pluginModule, function.toUtf8());
if(!pluginFunction || !PyCallable_Check(pluginFunction)) { if(!pluginFunction || !PyCallable_Check(pluginFunction)) {
PyErr_Clear(); PyErr_Clear();
Py_XDECREF(pluginFunction); Py_XDECREF(pluginFunction);
@ -719,84 +641,45 @@ bool PythonIntegrationPlugin::callPluginFunction(const QString &function, PyObje
return false; return false;
} }
Py_XINCREF(param1);
PyObject *pluginFunctionResult = PyObject_CallFunctionObjArgs(pluginFunction, param1, param2, param3, nullptr); Py_XINCREF(param2);
Py_XINCREF(param3);
Py_XDECREF(pluginFunction);
if (PyErr_Occurred()) {
qCWarning(dcThingManager()) << "Error calling python method:" << function << "on plugin" << pluginName();
PyErr_Print();
PyErr_Clear();
PyEval_ReleaseThread(m_threadState);
return false;
}
if (QByteArray(pluginFunctionResult->ob_type->tp_name) != "coroutine") {
Py_DECREF(pluginFunctionResult);
PyEval_ReleaseThread(m_threadState);
return true;
}
// Spawn a new event loop for the thread
QFuture<void> future = QtConcurrent::run([this, pluginFunctionResult, function](){
qCDebug(dcPythonIntegrations()) << "Spawning thread for" << function << "in plugin" << metadata().pluginName();
// Register this new thread in the interpreter
PyThreadState *thread = PyThreadState_New(m_threadState->interp);
// Acquire GIL and make the new thread state the current one
PyEval_RestoreThread(thread);
PyObject *new_event_loop = PyObject_GetAttrString(m_asyncio, "new_event_loop");
PyObject *loop = PyObject_CallFunctionObjArgs(new_event_loop, nullptr);
Py_DECREF(new_event_loop);
PyObject *create_task = PyObject_GetAttrString(loop, "create_task");
PyObject *task = PyObject_CallFunctionObjArgs(create_task, pluginFunctionResult, nullptr);
dumpError();
Py_DECREF(create_task);
Py_DECREF(pluginFunctionResult);
PyObject *add_done_callback = PyObject_GetAttrString(task, "add_done_callback");
dumpError();
PyObject *task_done = PyObject_GetAttrString(m_nymeaModule, "task_done");
PyObject *coroutineResult = PyObject_CallFunctionObjArgs(add_done_callback, task_done, nullptr);
dumpError();
Py_DECREF(coroutineResult);
Py_DECREF(add_done_callback);
Py_DECREF(task_done);
PyObject *run_until_complete = PyObject_GetAttrString(loop, "run_until_complete");
Py_DECREF(loop);
PyObject *taskResult = PyObject_CallFunctionObjArgs(run_until_complete, task, nullptr);
dumpError();
Py_XDECREF(taskResult);
Py_DECREF(run_until_complete);
Py_DECREF(task);
// Destroy the thread and release the GIL
PyThreadState_Clear(thread);
PyThreadState_DeleteCurrent();
qCDebug(dcPythonIntegrations()) << "Thread for" << function << "in plugin" << metadata().pluginName() << "ended";
});
QFutureWatcher<void> *watcher = new QFutureWatcher<void>(this); QFutureWatcher<void> *watcher = new QFutureWatcher<void>(this);
watcher->setFuture(future);
m_runningTasks.insert(watcher);
connect(watcher, &QFutureWatcher<void>::finished, this, [this, watcher](){ // Run the plugin function in the thread pool
QFuture<void> future = QtConcurrent::run(m_threadPool, [=](){
qCDebug(dcPythonIntegrations()) << "+++ Thread for" << function << "in plugin" << metadata().pluginName();
// Register this new thread in the interpreter
PyThreadState *threadState = PyThreadState_New(m_threadState->interp);
// Acquire GIL and make the new thread state the current one
PyEval_RestoreThread(threadState);
PyObject *pluginFunctionResult = PyObject_CallFunctionObjArgs(pluginFunction, param1, param2, param3, nullptr);
dumpError();
if (PyErr_Occurred()) {
qCWarning(dcThingManager()) << "Error calling python method:" << function << "on plugin" << pluginName();
}
Py_DECREF(pluginFunction);
Py_XDECREF(pluginFunctionResult);
Py_XDECREF(param1);
Py_XDECREF(param2);
Py_XDECREF(param3);
m_runningTasks.remove(watcher); m_runningTasks.remove(watcher);
delete watcher;
});
// Destroy the thread and release the GIL
PyThreadState_Clear(threadState);
PyEval_ReleaseThread(threadState);
PyThreadState_Delete(threadState);
qCDebug(dcPythonIntegrations()) << "--- Thread for" << function << "in plugin" << metadata().pluginName();
});
watcher->setFuture(future);
m_runningTasks.insert(watcher, function);
PyEval_ReleaseThread(m_threadState); PyEval_ReleaseThread(m_threadState);
return true; return true;

View File

@ -47,10 +47,6 @@ public:
static PyObject* pyAutoThingsAppeared(PyObject *self, PyObject* args); static PyObject* pyAutoThingsAppeared(PyObject *self, PyObject* args);
static PyObject* pyAutoThingDisappeared(PyObject *self, PyObject* args); static PyObject* pyAutoThingDisappeared(PyObject *self, PyObject* args);
public:
// python callbacks
static PyObject* task_done(PyObject* self, PyObject* args);
private: private:
void exportIds(); void exportIds();
void exportThingClass(const ThingClass &thingClass); void exportThingClass(const ThingClass &thingClass);
@ -66,17 +62,22 @@ private:
private: private:
// The main thread state in which we create an interpreter per plugin // The main thread state in which we create an interpreter per plugin
static PyThreadState* s_mainThreadState; static PyThreadState* s_mainThreadState;
static QThreadPool *s_threadPool;
// A per plugin thread state and interpreter // A per plugin thread state and interpreter
PyThreadState *m_threadState = nullptr; PyThreadState *m_threadState = nullptr;
// Modules imported into the interpreter // A per plugin thread pool
PyObject *m_nymeaModule; QThreadPool *m_threadPool = nullptr;
PyObject *m_asyncio;
// Running concurrent tasks in this plugins thread pool
QHash<QFutureWatcher<void>*, QString> m_runningTasks;
// The nymea module we import into the interpreter
PyObject *m_nymeaModule = nullptr;
// The imported plugin module (the plugin.py) // The imported plugin module (the plugin.py)
PyObject *m_module = nullptr; PyObject *m_pluginModule = nullptr;
PyObject *m_logger = nullptr;
// A map of plugin instances to plugin python scripts/modules // A map of plugin instances to plugin python scripts/modules
// Make sure to hold the GIL when accessing this. // Make sure to hold the GIL when accessing this.
@ -91,8 +92,6 @@ private:
// Need to keep a copy of plugin params and sync that in a thread-safe manner // Need to keep a copy of plugin params and sync that in a thread-safe manner
ParamList m_pluginConfigCopy; ParamList m_pluginConfigCopy;
// Running concurrent tasks in this plugin
QSet<QFutureWatcher<void>*> m_runningTasks;
}; };
#endif // PYTHONINTEGRATIONPLUGIN_H #endif // PYTHONINTEGRATIONPLUGIN_H

View File

@ -21,6 +21,7 @@ RESOURCES += $$top_srcdir/icons.qrc \
HEADERS += nymeacore.h \ HEADERS += nymeacore.h \
integrations/plugininfocache.h \ integrations/plugininfocache.h \
integrations/python/pynymealogginghandler.h \ integrations/python/pynymealogginghandler.h \
integrations/python/pynymeamodule.h \
integrations/python/pyparam.h \ integrations/python/pyparam.h \
integrations/python/pything.h \ integrations/python/pything.h \
integrations/python/pythingactioninfo.h \ integrations/python/pythingactioninfo.h \

View File

@ -1,14 +1,17 @@
import nymea import nymea
import asyncio import time
#from fastdotcom import fast_com #from fastdotcom import fast_com
watchingAutoThings = False watchingAutoThings = False
loopRunning = False
async def init(): def init():
logger.log("Python mock plugin init") logger.log("Python mock plugin init")
global loopRunning
loopRunning = True
while True: while loopRunning:
await asyncio.sleep(5); time.sleep(5);
for thing in myThings(): for thing in myThings():
if thing.thingClassId == pyMockThingClassId: if thing.thingClassId == pyMockThingClassId:
logger.log("Emitting event 1 for", thing.name, "eventTypeId", pyMockEvent1EventTypeId) logger.log("Emitting event 1 for", thing.name, "eventTypeId", pyMockEvent1EventTypeId)
@ -20,6 +23,13 @@ async def init():
thing.emitEvent(pyMockDiscoveryPairingEvent1EventTypeId, [nymea.Param(pyMockDiscoveryPairingEvent1EventParam1ParamTypeId, "Im an event")]) thing.emitEvent(pyMockDiscoveryPairingEvent1EventTypeId, [nymea.Param(pyMockDiscoveryPairingEvent1EventParam1ParamTypeId, "Im an event")])
logger.log("Setting state 1 for", thing.name, "Old value is:", thing.stateValue(pyMockDiscoveryPairingState1StateTypeId)) logger.log("Setting state 1 for", thing.name, "Old value is:", thing.stateValue(pyMockDiscoveryPairingState1StateTypeId))
thing.setStateValue(pyMockDiscoveryPairingState1StateTypeId, thing.stateValue(pyMockDiscoveryPairingState1StateTypeId) + 1) thing.setStateValue(pyMockDiscoveryPairingState1StateTypeId, thing.stateValue(pyMockDiscoveryPairingState1StateTypeId) + 1)
logger.log("Bye bye")
def deinit():
logger.log("shutting down")
global loopRunning
loopRunning = False
def configValueChanged(paramTypeId, value): def configValueChanged(paramTypeId, value):
@ -55,9 +65,9 @@ def startMonitoringAutoThings():
logger.log("Done start monitoring auto things") logger.log("Done start monitoring auto things")
async def discoverThings(info): def discoverThings(info):
logger.log("Discovery started for", info.thingClassId, "with result count:", info.params[0].value) logger.log("Discovery started for", info.thingClassId, "with result count:", info.params[0].value)
await asyncio.sleep(10) # Some delay for giving a feeling of a discovery time.sleep(10) # Some delay for giving a feeling of a discovery
# Add 2 new discovery results # Add 2 new discovery results
for i in range(0, info.params[0].value): for i in range(0, info.params[0].value):
info.addDescriptor(nymea.ThingDescriptor(pyMockDiscoveryPairingThingClassId, "Python mock thing %i" % i)) info.addDescriptor(nymea.ThingDescriptor(pyMockDiscoveryPairingThingClassId, "Python mock thing %i" % i))
@ -69,26 +79,26 @@ async def discoverThings(info):
info.finish(nymea.ThingErrorNoError) info.finish(nymea.ThingErrorNoError)
async def startPairing(info): def startPairing(info):
logger.log("startPairing for", info.thingName, info.thingId, info.params) logger.log("startPairing for", info.thingName, info.thingId, info.params)
info.finish(nymea.ThingErrorNoError, "Log in as user \"john\" with password \"smith\".") info.finish(nymea.ThingErrorNoError, "Log in as user \"john\" with password \"smith\".")
async def confirmPairing(info, username, secret): def confirmPairing(info, username, secret):
logger.log("confirming pairing for", info.thingName, username, secret) logger.log("confirming pairing for", info.thingName, username, secret)
await asyncio.sleep(1) time.sleep(1)
if username == "john" and secret == "smith": if username == "john" and secret == "smith":
info.finish(nymea.ThingErrorNoError) info.finish(nymea.ThingErrorNoError)
else: else:
info.finish(nymea.ThingErrorAuthenticationFailure, "Error logging in here!") info.finish(nymea.ThingErrorAuthenticationFailure, "Error logging in here!")
async def setupThing(info): def setupThing(info):
logger.log("setupThing for", info.thing.name) logger.log("setupThing for", info.thing.name)
info.finish(nymea.ThingErrorNoError) info.finish(nymea.ThingErrorNoError)
async def postSetupThing(thing): def postSetupThing(thing):
logger.log("postSetupThing for", thing.name) logger.log("postSetupThing for", thing.name)
thing.nameChangedHandler = lambda thing : logger.log("Thing name changed", thing.name) thing.nameChangedHandler = lambda thing : logger.log("Thing name changed", thing.name)