|
4 | 4 | #include "plugin.h"
|
5 | 5 | #include "pypluginloader.h"
|
6 | 6 | #include <QDir>
|
| 7 | +#include <QEventLoop> |
7 | 8 | #include <QFileInfo>
|
| 9 | +#include <QFontDatabase> |
8 | 10 | #include <QMessageBox>
|
| 11 | +#include <QProcess> |
9 | 12 | #include <QRegularExpression>
|
10 | 13 | #include <QStandardPaths>
|
| 14 | +#include <QTextEdit> |
11 | 15 | using namespace albert;
|
12 | 16 | using namespace std;
|
13 | 17 | namespace py = pybind11;
|
@@ -200,55 +204,110 @@ const albert::PluginMetaData &PyPluginLoader::metaData() const { return metadata
|
200 | 204 |
|
201 | 205 | albert::PluginInstance *PyPluginLoader::instance() const { return cpp_plugin_instance_; }
|
202 | 206 |
|
203 |
| -QString PyPluginLoader::load() |
| 207 | +QString PyPluginLoader::load_() |
204 | 208 | {
|
205 | 209 | py::gil_scoped_acquire acquire;
|
| 210 | + |
| 211 | + // Import as __name__ = albert.package_name |
| 212 | + py::module importlib_util = py::module::import("importlib.util"); |
| 213 | + py::object pyspec = importlib_util.attr("spec_from_file_location")(QString("albert.%1").arg(metadata_.id), source_path_); // Prefix to avoid conflicts |
| 214 | + module_ = importlib_util.attr("module_from_spec")(pyspec); |
| 215 | + |
| 216 | + // Set default md_id |
| 217 | + if (!py::hasattr(module_, ATTR_MD_ID)) |
| 218 | + module_.attr("md_id") = metadata_.id; |
| 219 | + |
| 220 | + // Attach logcat functions |
| 221 | + // https://bugreports.qt.io/browse/QTBUG-117153 |
| 222 | + // https://code.qt.io/cgit/pyside/pyside-setup.git/commit/?h=6.5&id=2823763072ce3a2da0210dbc014c6ad3195fbeff |
| 223 | + py::setattr(module_,"debug", py::cpp_function([this](const QString &s){ qCDebug((*logging_category)) << s; })); |
| 224 | + py::setattr(module_,"info", py::cpp_function([this](const QString &s){ qCInfo((*logging_category)) << s; })); |
| 225 | + py::setattr(module_,"warning", py::cpp_function([this](const QString &s){ qCWarning((*logging_category)) << s; })); |
| 226 | + py::setattr(module_,"critical", py::cpp_function([this](const QString &s){ qCCritical((*logging_category)) << s; })); |
| 227 | + |
| 228 | + // Execute module |
| 229 | + pyspec.attr("loader").attr("exec_module")(module_); |
| 230 | + |
| 231 | + // Instanciate plugin |
| 232 | + py_plugin_instance_ = module_.attr(ATTR_PLUGIN_CLASS)(); // may throw |
| 233 | + cpp_plugin_instance_ = py_plugin_instance_.cast<PluginInstance*>(); // may throw |
| 234 | + |
| 235 | + for (const auto& exec : metadata_.binary_dependencies) |
| 236 | + if (QStandardPaths::findExecutable(exec).isNull()) |
| 237 | + throw runtime_error(QString("No '%1' in $PATH.").arg(exec).toStdString()); |
| 238 | + |
| 239 | + return {}; |
| 240 | +} |
| 241 | + |
| 242 | +QString PyPluginLoader::load() |
| 243 | +{ |
206 | 244 | QString err;
|
207 | 245 | try
|
208 | 246 | {
|
209 |
| - // Import as __name__ = albert.package_name |
210 |
| - py::module importlib_util = py::module::import("importlib.util"); |
211 |
| - py::object pyspec = importlib_util.attr("spec_from_file_location")(QString("albert.%1").arg(metadata_.id), source_path_); // Prefix to avoid conflicts |
212 |
| - module_ = importlib_util.attr("module_from_spec")(pyspec); |
213 |
| - |
214 |
| - // Set default md_id |
215 |
| - if (!py::hasattr(module_, ATTR_MD_ID)) |
216 |
| - module_.attr("md_id") = metadata_.id; |
217 |
| - |
218 |
| - // Attach logcat functions |
219 |
| - // https://bugreports.qt.io/browse/QTBUG-117153 |
220 |
| - // https://code.qt.io/cgit/pyside/pyside-setup.git/commit/?h=6.5&id=2823763072ce3a2da0210dbc014c6ad3195fbeff |
221 |
| - py::setattr(module_,"debug", py::cpp_function([this](const QString &s){ qCDebug((*logging_category)) << s; })); |
222 |
| - py::setattr(module_,"info", py::cpp_function([this](const QString &s){ qCInfo((*logging_category)) << s; })); |
223 |
| - py::setattr(module_,"warning", py::cpp_function([this](const QString &s){ qCWarning((*logging_category)) << s; })); |
224 |
| - py::setattr(module_,"critical", py::cpp_function([this](const QString &s){ qCCritical((*logging_category)) << s; })); |
225 |
| - |
226 |
| - // Execute module |
227 |
| - pyspec.attr("loader").attr("exec_module")(module_); |
228 |
| - |
229 |
| - // Instanciate plugin |
230 |
| - py_plugin_instance_ = module_.attr(ATTR_PLUGIN_CLASS)(); // may throw |
231 |
| - cpp_plugin_instance_ = py_plugin_instance_.cast<PluginInstance*>(); // may throw |
232 |
| - |
233 |
| - for (const auto& exec : metadata_.binary_dependencies) |
234 |
| - if (QStandardPaths::findExecutable(exec).isNull()) |
235 |
| - throw runtime_error(QString("No '%1' in $PATH.").arg(exec).toStdString()); |
236 |
| - |
237 |
| - return {}; |
238 |
| - |
239 |
| - } catch (py::error_already_set &e) { |
240 |
| - if (e.matches(PyExc_ModuleNotFoundError)) { |
241 |
| - auto text = QString::fromStdString(e.what()).section("\n",0,0); |
242 |
| - text.append(QString("\n\nTry installing missing dependencies (%1) into albert site-packages?\n\n" |
243 |
| - "Note that you have to reload the plugin afterwards.").arg(metadata_.runtime_dependencies.join(", "))); |
244 |
| - auto b = QMessageBox::warning(nullptr, "Module not found", text, |
245 |
| - QMessageBox::Yes|QMessageBox::No, QMessageBox::Yes); |
246 |
| - if (b==QMessageBox::Yes) |
247 |
| - provider_.installPackages(metadata_.runtime_dependencies); |
248 |
| - |
249 |
| - err = text; |
250 |
| - } else |
251 |
| - err = e.what(); |
| 247 | + try { |
| 248 | + return load_(); |
| 249 | + } catch (py::error_already_set &e) { |
| 250 | + |
| 251 | + // Catch only import errors, rethrow anything else |
| 252 | + if (e.matches(PyExc_ModuleNotFoundError)) { |
| 253 | + |
| 254 | + // ask user if dependencies should be installed |
| 255 | + QMessageBox mb; |
| 256 | + mb.setIcon(QMessageBox::Information); |
| 257 | + mb.setWindowTitle("Module not found"); |
| 258 | + mb.setText("Some modules were not found. Probably this plugin has mandatory " |
| 259 | + "dependencies which are not installed on this system.\n\n" |
| 260 | + "Install dependencies into the Albert virtual environment?"); |
| 261 | + mb.setStandardButtons(QMessageBox::Yes|QMessageBox::No); |
| 262 | + mb.setDefaultButton(QMessageBox::Yes); |
| 263 | + mb.setDetailedText(e.what()); |
| 264 | + if (mb.exec() == QMessageBox::Yes){ |
| 265 | + |
| 266 | + // install dependencies |
| 267 | + |
| 268 | + QProcess proc; |
| 269 | + proc.start( |
| 270 | + "python3", |
| 271 | + { |
| 272 | + "-m", |
| 273 | + "pip", |
| 274 | + "install", |
| 275 | + "--disable-pip-version-check", |
| 276 | + "--target", |
| 277 | + provider_.dataDir()->filePath("site-packages"), |
| 278 | + metadata_.runtime_dependencies.join(" ") |
| 279 | + } |
| 280 | + ); |
| 281 | + |
| 282 | + auto *te = new QTextEdit; |
| 283 | + te->setCurrentFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); |
| 284 | + te->setReadOnly(true); |
| 285 | + te->setWindowTitle(QString("Installing '%1' dependencies").arg(metadata_.name)); |
| 286 | + te->resize(600, 480); |
| 287 | + te->setAttribute(Qt::WA_DeleteOnClose); |
| 288 | + te->show(); |
| 289 | + |
| 290 | + QObject::connect(&proc, &QProcess::readyReadStandardOutput, te, |
| 291 | + [te, &proc](){ te->append(QString::fromUtf8(proc.readAllStandardOutput())); }); |
| 292 | + QObject::connect(&proc, &QProcess::readyReadStandardError, te, |
| 293 | + [te, &proc](){ te->append(QString::fromUtf8(proc.readAllStandardError())); }); |
| 294 | + |
| 295 | + QEventLoop loop; |
| 296 | + QObject::connect(&proc, &QProcess::finished, &loop, &QEventLoop::quit); |
| 297 | + loop.exec(); |
| 298 | + |
| 299 | + if (proc.exitStatus() == QProcess::ExitStatus::NormalExit && proc.exitCode() == EXIT_SUCCESS){ |
| 300 | + |
| 301 | + te->deleteLater(); // auto-close on success only |
| 302 | + return load_(); // On success try again |
| 303 | + |
| 304 | + } else |
| 305 | + err = "Installing dependencies failed."; |
| 306 | + } else |
| 307 | + err = "Dependencies are missing."; |
| 308 | + } else |
| 309 | + throw e; |
| 310 | + } |
252 | 311 | } catch(const std::exception &e) {
|
253 | 312 | err = e.what();
|
254 | 313 | } catch(...) {
|
|
0 commit comments