|
27 | 27 | #include "Runtime/Runtime.h" |
28 | 28 | #include "Utilities.h" |
29 | 29 |
|
30 | | -#include <QDesktopServices> |
31 | | -#include <QUrl> |
32 | | - |
33 | 30 | #include <pybind11/pytypes.h> |
34 | 31 |
|
35 | 32 | #define slots Q_SLOTS |
36 | 33 | #define signals Q_SIGNALS |
37 | 34 | #include <QCoreApplication> |
| 35 | +#include <QDesktopServices> |
38 | 36 | #include <QDialog> |
39 | 37 | #include <QFile> |
40 | 38 | #include <QFileDialog> |
41 | 39 | #include <QMenu> |
| 40 | +#include <QRunnable> |
42 | 41 | #include <QSettings> |
| 42 | +#include <QThreadPool> |
| 43 | +#include <QUrl> |
| 44 | + |
43 | 45 | #include <algorithm> |
44 | 46 | #include <ccCommandLineInterface.h> |
45 | 47 |
|
| 48 | +/// Loading Python plugins may be slow, |
| 49 | +/// we want cloudcompare to open as fast as possible |
| 50 | +/// so we load plugins in a separate thread |
| 51 | +class LoadPluginsTask : public QObject, public QRunnable |
| 52 | +{ |
| 53 | + Q_OBJECT |
| 54 | + |
| 55 | + public: |
| 56 | + explicit LoadPluginsTask(PythonPluginManager &pluginManager, const QStringList &pluginsPaths) |
| 57 | + : m_pluginManager(pluginManager), m_pluginsPaths(pluginsPaths) |
| 58 | + { |
| 59 | + } |
| 60 | + |
| 61 | + void run() override |
| 62 | + { |
| 63 | + py::gil_scoped_acquire acquire; |
| 64 | + m_pluginManager.loadPlugins(m_pluginsPaths); |
| 65 | + Q_EMIT finished(); |
| 66 | + } |
| 67 | + |
| 68 | + Q_SIGNALS: |
| 69 | + void finished(); |
| 70 | + |
| 71 | + private: |
| 72 | + // References are fine here as this object is short-lived |
| 73 | + PythonPluginManager &m_pluginManager; |
| 74 | + const QStringList m_pluginsPaths; |
| 75 | +}; |
| 76 | + |
46 | 77 | // Useful link: |
47 | 78 | // https://docs.python.org/3/c-api/init.html#initialization-finalization-and-threads |
48 | 79 | PythonPlugin::PythonPlugin(QObject *parent) |
@@ -110,6 +141,10 @@ PythonPlugin::PythonPlugin(QObject *parent) |
110 | 141 | this, |
111 | 142 | &PythonPlugin::finalizeInterpreter); |
112 | 143 | } |
| 144 | + |
| 145 | + // The REPL action is created here, as we need it to exist in |
| 146 | + // order to enable / disable it depending on circumstances |
| 147 | + m_showRepl = new QAction("Show REPL", this); |
113 | 148 | } |
114 | 149 |
|
115 | 150 | static std::unique_ptr<QSettings> LoadSettings() |
@@ -140,14 +175,9 @@ QList<QAction *> PythonPlugin::getActions() |
140 | 175 | m_showEditor->setEnabled(isPythonProperlyInitialized); |
141 | 176 | } |
142 | 177 |
|
143 | | - if (!m_showRepl) |
144 | | - { |
145 | | - m_showRepl = new QAction("Show REPL", this); |
146 | | - m_showRepl->setToolTip("Show the Python REPL"); |
147 | | - m_showRepl->setIcon(QIcon(REPL_ICON_PATH)); |
148 | | - connect(m_showRepl, &QAction::triggered, this, &PythonPlugin::showRepl); |
149 | | - m_showRepl->setEnabled(isPythonProperlyInitialized); |
150 | | - } |
| 178 | + m_showRepl->setToolTip("Show the Python REPL"); |
| 179 | + m_showRepl->setIcon(QIcon(REPL_ICON_PATH)); |
| 180 | + connect(m_showRepl, &QAction::triggered, this, &PythonPlugin::showRepl); |
151 | 181 |
|
152 | 182 | if (!m_showDoc) |
153 | 183 | { |
@@ -497,28 +527,28 @@ void PythonPlugin::setMainAppInterface(ccMainAppInterface *app) |
497 | 527 | // python plugins, if we did this earlier, `pycc.GetInstance()` |
498 | 528 | // in python would return `None` and that's bad. |
499 | 529 |
|
500 | | - // Start by autodiscovering plugins from metadata |
501 | | - try |
502 | | - { |
503 | | - m_pluginManager.loadPluginsFromEntryPoints(); |
504 | | - } |
505 | | - catch (const std::exception &e) |
506 | | - { |
507 | | - ccLog::Warning("[PythonRuntime] Failed to load autodiscovered custom python plugins: %s", |
508 | | - e.what()); |
509 | | - } |
510 | | - |
511 | | - // In the end, we add plugins from custom paths |
512 | | - try |
513 | | - { |
514 | | - m_pluginManager.loadPluginsFrom(m_settings->pluginsPaths()); |
515 | | - } |
516 | | - catch (const std::exception &e) |
517 | | - { |
518 | | - ccLog::Warning("[PythonRuntime] Failed to load custom python plugins : %s", e.what()); |
519 | | - } |
520 | | - |
521 | | - populatePluginSubMenu(); |
| 530 | + // We load the plugins in a separate thread to avoid the app from being too slow to start |
| 531 | + // However, the thread that will load plugins will need to acquire the GIL, |
| 532 | + // which the interpreter on the main thread currently owns. |
| 533 | + // So we release it first, and re-acquire it as soon as plugin loading is done |
| 534 | + auto *gilReleaser = new py::gil_scoped_release(); |
| 535 | + // While plugins are loading it's not possible to run any python code |
| 536 | + Q_EMIT m_interp.executionStarted(); |
| 537 | + m_showRepl->setEnabled(false); // repl is slight harder to handle |
| 538 | + auto *task = new LoadPluginsTask(m_pluginManager, m_settings->pluginsPaths()); |
| 539 | + QObject::connect(task, |
| 540 | + &LoadPluginsTask::finished, |
| 541 | + this, |
| 542 | + [gilReleaser, this]() |
| 543 | + { |
| 544 | + delete gilReleaser; |
| 545 | + Q_EMIT m_interp.executionFinished(); |
| 546 | + this->populatePluginSubMenu(); |
| 547 | + m_showRepl->setEnabled(true); |
| 548 | + plgPrint() << "Plugins loaded, running code is enabled"; |
| 549 | + }); |
| 550 | + plgPrint() << "Loading plugins, running code is disabled"; |
| 551 | + QThreadPool::globalInstance()->start(task); |
522 | 552 |
|
523 | 553 | m_fileRunner->setParent(m_app->getMainWindow(), Qt::Window); |
524 | 554 | m_actionLauncher->setParent(m_app->getMainWindow(), Qt::Window); |
@@ -671,3 +701,5 @@ void PythonPlugin::finalizeInterpreter() |
671 | 701 | m_pluginManager.unloadPlugins(); |
672 | 702 | m_interp.finalize(); |
673 | 703 | } |
| 704 | + |
| 705 | +#include "PythonPlugin.moc" |
0 commit comments