Skip to content

Commit

Permalink
feat: Better error logging for plugin backends
Browse files Browse the repository at this point in the history
  • Loading branch information
shdwmtr committed Nov 9, 2024
1 parent e8e9790 commit 45433c0
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 22 deletions.
47 changes: 40 additions & 7 deletions src/core/co_initialize/co_stub.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <core/loader.h>
#include <core/hooks/web_load.h>
#include <core/ffi/ffi.h>
#include <tuple>

const std::string GetBootstrapModule(const std::string scriptModules, const uint16_t port)
{
Expand Down Expand Up @@ -156,21 +157,35 @@ void AddSitePackagesDirectory(std::filesystem::path customPath)

/// @brief initializes the current plugin. creates a plugin instance and calls _load()
/// @param global_dict
void StartPluginBackend(PyObject* global_dict)
void StartPluginBackend(PyObject* global_dict, std::string pluginName)
{
const auto PrintError = [&pluginName]()
{
const auto [errorMessage, traceback] = Python::GetExceptionInformaton();
PyErr_Clear();

if (errorMessage == "name 'plugin' is not defined")
{
Logger.PrintMessage(" FFI-ERROR ", fmt::format("Millennium failed to call _load on {}", pluginName), COL_RED);
return;
}

Logger.PrintMessage(" FFI-ERROR ", fmt::format("Millennium failed to call _load on {}: {}\n{}{}", pluginName, COL_RED, traceback, COL_RESET), COL_RED);
};

PyObject *pluginComponent = PyDict_GetItemString(global_dict, "Plugin");

if (!pluginComponent || !PyCallable_Check(pluginComponent))
{
PyErr_Print();
PrintError();
return;
}

PyObject *pluginComponentInstance = PyObject_CallObject(pluginComponent, NULL);

if (!pluginComponentInstance)
{
PyErr_Print();
PrintError();
return;
}

Expand All @@ -179,7 +194,7 @@ void StartPluginBackend(PyObject* global_dict)

if (!loadMethodAttribute || !PyCallable_Check(loadMethodAttribute))
{
PyErr_Print();
PrintError();
return;
}

Expand Down Expand Up @@ -237,15 +252,33 @@ const void CoInitializer::BackendStartCallback(SettingsStore::PluginTypeSchema p
return;
}

if (PyRun_SimpleFile(mainModuleFilePtr, backendMainModule.c_str()) != 0)
PyObject* mainModule = PyImport_AddModule("__main__");
PyObject* mainModuleDict = PyModule_GetDict(mainModule);

if (!mainModule || !mainModuleDict) {
Logger.Warn("Millennium failed to initialize the main module.");
backendHandler.BackendLoaded({ plugin.pluginName, CoInitializer::BackendCallbacks::BACKEND_LOAD_FAILED });
fclose(mainModuleFilePtr);
return;
}

PyObject* result = PyRun_File(mainModuleFilePtr, backendMainModule.c_str(), Py_file_input, mainModuleDict, mainModuleDict);
fclose(mainModuleFilePtr);

if (!result)
{
const auto [errorMessage, traceback] = Python::GetExceptionInformaton();

Logger.PrintMessage(" PY-MAN ", fmt::format("Millennium failed to start {}: {}\n{}{}", plugin.pluginName, COL_RED, traceback, COL_RESET), COL_RED);

PyErr_Print(); // Print the Python error to stderr
Logger.Warn("Millennium failed to start '{}'. This is likely due to failing module side effects, unrelated to Millennium.", plugin.pluginName);
backendHandler.BackendLoaded({ plugin.pluginName, CoInitializer::BackendCallbacks::BACKEND_LOAD_FAILED });

return;
}

StartPluginBackend(globalDictionary);
Py_DECREF(result);
StartPluginBackend(globalDictionary, plugin.pluginName);
}

const std::string ConstructScriptElement(std::string filename)
Expand Down
86 changes: 71 additions & 15 deletions src/core/ffi/c_python.cc
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "ffi.h"
#include <core/py_controller/co_spawn.h>
#include <iostream>
#include <tuple>

std::string Python::ConstructFunctionCall(nlohmann::basic_json<> data)
{
Expand Down Expand Up @@ -35,28 +36,67 @@ std::string Python::ConstructFunctionCall(nlohmann::basic_json<> data)
return strFunctionCall;
}

std::tuple<std::string, std::string> Python::GetExceptionInformaton()
{
PyObject* typeObj = nullptr;
PyObject* valueObj = nullptr;
PyObject* traceBackObj = nullptr;

PyErr_Fetch(&typeObj, &valueObj, &traceBackObj);
PyErr_NormalizeException(&typeObj, &valueObj, &traceBackObj);

if (!valueObj)
{
return { "Unknown Error.", "Unknown Error." };
}

PyObject* pStrErrorMessage = PyObject_Str(valueObj);
const char* errorMessage = pStrErrorMessage ? PyUnicode_AsUTF8(pStrErrorMessage) : "Unknown Error.";

std::string tracebackText;
if (traceBackObj)
{
PyObject* tracebackModule = PyImport_ImportModule("traceback");
if (tracebackModule)
{
PyObject* formatExceptionFunc = PyObject_GetAttrString(tracebackModule, "format_exception");
if (formatExceptionFunc && PyCallable_Check(formatExceptionFunc))
{
PyObject* tracebackList = PyObject_CallFunctionObjArgs(formatExceptionFunc, typeObj, valueObj, traceBackObj, NULL);
if (tracebackList)
{
PyObject* tracebackStr = PyUnicode_Join(PyUnicode_FromString(""), tracebackList);
if (tracebackStr)
{
tracebackText = PyUnicode_AsUTF8(tracebackStr);
Py_DECREF(tracebackStr);
}
Py_DECREF(tracebackList);
}
Py_DECREF(formatExceptionFunc);
}
Py_DECREF(tracebackModule);
}
}

Py_XDECREF(typeObj);
Py_XDECREF(valueObj);
Py_XDECREF(traceBackObj);
Py_XDECREF(pStrErrorMessage);

return { errorMessage, tracebackText };
}

const Python::EvalResult EvaluatePython(std::string pluginName, std::string script)
{
PyObject* globalDictionaryObj = PyModule_GetDict(PyImport_AddModule("__main__"));
PyObject* EvaluatedObj = PyRun_String(script.c_str(), Py_eval_input, globalDictionaryObj, globalDictionaryObj);

if (!EvaluatedObj && PyErr_Occurred())
{
PyObject* typeObj, *valueObj, *traceBackObj;
PyErr_Fetch(&typeObj, &valueObj, &traceBackObj);
PyErr_NormalizeException(&typeObj, &valueObj, &traceBackObj);

PyObject* pStrErrorMessage = PyObject_Str(valueObj);
if (pStrErrorMessage)
{
const char* errorMessage = PyUnicode_AsUTF8(pStrErrorMessage);
Python::EvalResult exceptionResult = { errorMessage ? errorMessage : "Unknown Error.", Python::Types::Error };
const auto [errorMessage, traceback] = Python::GetExceptionInformaton();

Logger.PrintMessage(" FFI-ERR ", fmt::format("An error occurred while calling a backend method from [{}].", pluginName), COL_RED);
Logger.PrintMessage(" TRACE ", fmt::format("{} -> {}", script, exceptionResult.plain), COL_RED);

return exceptionResult;
}
Logger.PrintMessage(" FFI-ERROR ", fmt::format("Failed to call {}: {}\n{}{}", script, COL_RED, traceback, COL_RESET), COL_RED);
}

if (EvaluatedObj == nullptr || EvaluatedObj == Py_None)
Expand Down Expand Up @@ -124,7 +164,23 @@ void Python::LockGILAndDiscardEvaluate(std::string pluginName, std::string scrip
std::shared_ptr<PythonGIL> pythonGilLock = std::make_shared<PythonGIL>();
pythonGilLock->HoldAndLockGILOnThread(threadState);
{
PyRun_SimpleString(script.c_str());
PyObject* globalDictionaryObj = PyModule_GetDict(PyImport_AddModule("__main__"));
PyObject* EvaluatedObj = PyRun_String(script.c_str(), Py_eval_input, globalDictionaryObj, globalDictionaryObj);

if (!EvaluatedObj && PyErr_Occurred())
{
const auto [errorMessage, traceback] = Python::GetExceptionInformaton();
PyErr_Clear();

if (errorMessage == "name 'plugin' is not defined")
{
Logger.PrintMessage(" FFI-ERROR ", fmt::format("Millennium failed to call {} on {} as the function "
"does not exist, or the interpreter crashed before it was loaded.", script, pluginName), COL_RED);
return;
}

Logger.PrintMessage(" FFI-ERROR ", fmt::format("Millennium failed to call {} on {}: {}\n{}{}", script, pluginName, COL_RED, traceback, COL_RESET), COL_RED);
}
}
pythonGilLock->ReleaseAndUnLockGIL();
}
1 change: 1 addition & 0 deletions src/core/ffi/ffi.h
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ namespace Python {
};

std::string ConstructFunctionCall(nlohmann::basic_json<> data);
std::tuple<std::string, std::string> GetExceptionInformaton();

EvalResult LockGILAndEvaluate(std::string pluginName, std::string script);
void LockGILAndDiscardEvaluate(std::string pluginName, std::string script);
Expand Down

0 comments on commit 45433c0

Please sign in to comment.