RTXI  3.0.0
The Real-Time eXperiment Interface Reference Manual
rtxi_wizard.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2011 Georgia Institute of Technology, University of Utah,
3  Weill Cornell Medical College
4 
5  This program is free software: you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation, either version 3 of the License, or
8  (at your option) any later version.
9 
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  GNU General Public License for more details.
14 
15  You should have received a copy of the GNU General Public License
16  along with this program. If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include <QCoreApplication>
20 #include <QDir>
21 #include <QFileInfo>
22 #include <QJsonArray>
23 #include <QJsonDocument>
24 #include <QJsonObject>
25 #include <QMessageBox>
26 #include <QProcess>
27 #include <QtGlobal>
28 #include <iostream>
29 
30 #include "rtxi_wizard.hpp"
31 
32 #include <unistd.h>
33 
34 #include "rtxiConfig.h"
35 
36 /*
37  * This module uses the QNetworkManager class to fetch information on our
38  * GitHub repos using GitHub's own API.
39  */
40 RTXIWizard::Panel::Panel(QMainWindow* mwindow, Event::Manager* ev_manager)
41  : Widgets::Panel(std::string(RTXIWizard::MODULE_NAME), mwindow, ev_manager)
42  , readmeWindow(new QTextEdit)
43  , availableListWidget(new QListWidget(this))
44 {
45  setWhatsThis(
46  "Module Wizard enables management of all RTXI modules. You can download "
47  "and install new modules directly from the GitHub site..etc");
48  install_prefix.setPath(QCoreApplication::applicationDirPath()
49  + QString("/rtxi_modules/"));
50  auto* customLayout = new QGridLayout;
51 
52  auto* buttonBox = new QGroupBox;
53  auto* buttonLayout = new QHBoxLayout();
54  buttonBox->setLayout(buttonLayout);
55  syncButton = new QPushButton("Sync", this);
56  cloneButton = new QPushButton("Install", this);
57  cloneButton->setEnabled(false);
58  buttonLayout->addWidget(syncButton);
59  buttonLayout->addWidget(cloneButton);
60 
61  auto* installedBox = new QGroupBox("Installed");
62  auto* installedLayout = new QVBoxLayout;
63  installedBox->setLayout(installedLayout);
64  installedListWidget = new QListWidget(installedBox);
65  installedListWidget->setFixedWidth(175);
66  installedListWidget->setSortingEnabled(true);
67  installedLayout->addWidget(installedListWidget);
68 
69  auto* moduleBox = new QGroupBox("Available");
70  auto* moduleLayout = new QVBoxLayout;
71  moduleBox->setLayout(moduleLayout);
72 
73  availableListWidget->setFixedWidth(175);
74  availableListWidget->setSortingEnabled(true);
75  moduleLayout->addWidget(availableListWidget);
76 
77  readmeWindow->setReadOnly(true);
78  readmeWindow->show();
79 
80  customLayout->addWidget(buttonBox, 0, 0);
81  customLayout->addWidget(moduleBox, 1, 0);
82  customLayout->addWidget(installedBox, 2, 0);
83  customLayout->addWidget(readmeWindow, 0, 1, 3, 1);
84  customLayout->setColumnStretch(0, 0);
85  customLayout->setColumnStretch(1, 1);
86 
87  QObject::connect(syncButton, SIGNAL(clicked()), this, SLOT(getRepos()));
88  QObject::connect(cloneButton, SIGNAL(clicked()), this, SLOT(cloneModule()));
89  QObject::connect(availableListWidget,
90  SIGNAL(itemClicked(QListWidgetItem*)),
91  this,
92  SLOT(getReadme()));
93  QObject::connect(availableListWidget,
94  SIGNAL(itemClicked(QListWidgetItem*)),
95  this,
96  SLOT(updateButton()));
97  QObject::connect(installedListWidget,
98  SIGNAL(itemClicked(QListWidgetItem*)),
99  this,
100  SLOT(getReadme()));
101  QObject::connect(installedListWidget,
102  SIGNAL(itemClicked(QListWidgetItem*)),
103  this,
104  SLOT(updateButton()));
105 
106  setLayout(customLayout);
107  setWindowTitle("Module Wizard");
108  getRepos();
109  initParameters();
110  getMdiWindow()->resize(1000, 800);
111 }
112 
113 void RTXIWizard::Panel::initParameters()
114 {
115  // syntax here only works in c++11
116  exclude_list = std::vector<QString>({QString("rtxi"),
117  QString("rtxi.github.io"),
118  QString("genicam-camera"),
119  QString("rtxi-crawler"),
120  QString("matlab-tools"),
121  QString("tutorials"),
122  QString("autapse"),
123  QString("camera-control"),
124  QString("gen-net"),
125  QString("dynamo-examples"),
126  QString("plot-lib"),
127  QString("python-plugin"),
128  QString("poster"),
129  QString("user-manual"),
130  QString("logos"),
131  QString("live-image"),
132  QString("conference-2015")});
133  button_mode = DOWNLOAD;
134 }
135 
136 // Set the text to "Update" if the module is already installed or "Download
137 // and Install" if not.
138 void RTXIWizard::Panel::updateButton()
139 {
140  auto* parent = qobject_cast<QListWidget*>(sender());
141 
142  if (parent == availableListWidget) {
143  cloneButton->setText("Install");
144  button_mode = DOWNLOAD;
145  } else if (parent == installedListWidget) {
146  cloneButton->setText("Update");
147  button_mode = UPDATE;
148  }
149 }
150 
151 // Clone the module currently highlighted in the QListWidget.
152 void RTXIWizard::Panel::cloneModule()
153 {
154  cloneButton->setEnabled(false);
155  availableListWidget->setDisabled(true);
156  installedListWidget->setDisabled(true);
157  // QApplication::processEvents();
158 
159  QString name;
160  switch (button_mode) {
161  case DOWNLOAD:
162  name = availableListWidget->currentItem()->text();
163  break;
164  case UPDATE:
165  name = installedListWidget->currentItem()->text();
166  break;
167  default:
168  ERROR_MSG("ERROR: default in switch block in cloneModule()");
169  break;
170  }
171 
172  installFromString(name.toStdString());
173 }
174 
175 // Download the list of repos from GitHub's API. Call parseRepos for the JSON.
176 void RTXIWizard::Panel::getRepos()
177 {
178  availableListWidget->setDisabled(true);
179  installedListWidget->setDisabled(true);
180 
181  if (availableListWidget->count() == 0) {
182  const QUrl url("https://api.github.com/orgs/rtxi/repos?per_page=100");
183  reposNetworkReply = qnam.get(QNetworkRequest(url));
184  QObject::connect(
185  reposNetworkReply, SIGNAL(finished()), this, SLOT(parseRepos()));
186  } else {
187  availableListWidget->setDisabled(false);
188  installedListWidget->setDisabled(false);
189  }
190 }
191 
192 /*
193  * Download the README (markdown) for the highlighted repo just clicked. If the
194  * README has already been downloaded, the function will just keep that and
195  * not make another network request.
196  *
197  * The module doesn't save READMEs for after it closes. If you reopen the
198  * module, you'll have to redownload the repos.
199  */
200 void RTXIWizard::Panel::getReadme()
201 {
202  auto* parent = qobject_cast<QListWidget*>(sender());
203  if (parent->currentRow() < 0) {
204  return;
205  }
206  const QString name = parent->currentItem()->text();
207  availableListWidget->setDisabled(true);
208  installedListWidget->setDisabled(true);
209 
210  // If the README hasn't been downloaded before, download it now.
211  if (modules[parent->currentItem()->text()].readme == "") {
212  readmeNetworkReply = qnam.get(QNetworkRequest(modules[name].readme_url));
213  QObject::connect(
214  readmeNetworkReply, SIGNAL(finished()), this, SLOT(parseReadme()));
215  } else {
216  // Disable buttons until all logic is done.
217 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
218  readmeWindow->setText(modules[parent->currentItem()->text()].readme);
219 #else
220  readmeWindow->setMarkdown(modules[parent->currentItem()->text()].readme);
221 #endif
222  availableListWidget->setDisabled(false);
223  installedListWidget->setDisabled(false);
224  }
225 }
226 
227 // READMEs are downloaded as markdown. Convert them to HTML and display them
228 // within a QTextWidget.
229 void RTXIWizard::Panel::parseReadme()
230 {
231  const QByteArray network_reply_data = readmeNetworkReply->readAll();
232  const QString markdown_data = QString(network_reply_data.constData());
233 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
234  this->readmeWindow->setText(markdown_data);
235 #else
236  this->readmeWindow->setMarkdown(markdown_data);
237 #endif
238 
239  switch (button_mode) {
240  case DOWNLOAD:
241  modules[availableListWidget->currentItem()->text()].readme =
242  markdown_data;
243  break;
244  case UPDATE:
245  modules[installedListWidget->currentItem()->text()].readme =
246  markdown_data;
247  break;
248  default:
249  ERROR_MSG("ERROR: default in switch block in cloneModule()");
250  break;
251  }
252 
253  // readmeWindow->setHtml(fileText);
254  readmeWindow->show();
255  readmeNetworkReply->deleteLater();
256 
257  // The README is now displayed, so free the user to start clicking around.
258  cloneButton->setEnabled(true);
259  availableListWidget->setDisabled(false);
260  installedListWidget->setDisabled(false);
261 }
262 
263 // GitHub's API returns a JSON array. Parse it with QtJson functions.
264 void RTXIWizard::Panel::parseRepos()
265 {
266  const QJsonDocument jsonDoc =
267  QJsonDocument::fromJson(reposNetworkReply->readAll().data());
268  const QJsonArray jsonArr = jsonDoc.array();
269 
270  const QString readmeUrlPrefix = "https://raw.githubusercontent.com/RTXI/";
271  const QString readmeUrlSuffix = "/master/README.md";
272 
273  for (auto&& idx : jsonArr) {
274  QJsonObject newObj = (idx).toObject();
275  newObj.find("name").key();
276 
277  // if the current module isn't in the exclude_list
278  if (std::find(exclude_list.begin(),
279  exclude_list.end(),
280  newObj.value("name").toString())
281  == exclude_list.end())
282  {
283  module_t module;
284 
285  const QString name = newObj.value("name").toString();
286  module.readme_url = QUrl(readmeUrlPrefix + newObj.value("name").toString()
287  + readmeUrlSuffix);
288  module.clone_url = QUrl(newObj.value("clone_url").toString());
289  module.readme = "";
290  module.installed =
291  QFileInfo::exists(install_prefix.path() + QString("/")
292  + QString("lib") + name + QString(".so"));
293  modules[name] = module;
294  }
295  }
296 
297  reposNetworkReply->deleteLater();
298 
299  rebuildListWidgets();
300  availableListWidget->setDisabled(false);
301  installedListWidget->setDisabled(false);
302 }
303 
305 {
306  availableListWidget->clear();
307  installedListWidget->clear();
308 
309  for (auto& module : modules) {
310  if (module.second.installed) {
311  installedListWidget->addItem(module.first);
312  } else {
313  availableListWidget->addItem(module.first);
314  }
315  }
316 
317  installedListWidget->sortItems(Qt::AscendingOrder);
318  availableListWidget->sortItems(Qt::AscendingOrder);
319 }
320 
321 /*
322  * Public function, not for use in this module. It gets called by other
323  * classes. The function is basically a truncated version of cloneModule().
324  */
325 void RTXIWizard::Panel::installFromString(const std::string& module_name)
326 {
327  const QString name = QString(module_name.c_str());
328  // Let the user know that RTXI is installing the plugin.
329  auto* progress =
330  new QProgressDialog("Installing plugin", "Cancel", 0, 5, this);
331  progress->setMinimumDuration(0);
332  progress->setWindowModality(Qt::WindowModal);
333  progress->setLabelText("Starting...");
334  /*
335  * Two QByteArray variables are needed due to the way Qt stores binary data.
336  * Calling module->getCloneUrl().toString().toLatin1().data() will produce
337  * an error.
338  */
339  const QByteArray url = modules[name].clone_url.toString().toLatin1();
340  const QByteArray installpath = install_prefix.path().toLatin1();
341  const QString mod_location =
342  QDir::temp().absolutePath() + QDir::separator() + QString("rtxi_modules");
343  const QString source_location =
344  mod_location + QDir::separator() + QString(module_name.c_str());
345  QDir(source_location).removeRecursively();
346 
347  progress->setLabelText("Downloading extension...");
348  progress->setValue(1);
349  // QApplication::processEvents();
350  // If the repo already exists, pull from master. If not, clone it.
351  auto* command = new QProcess();
352  const QStringList clone_args = {
353  "clone",
354  "-b",
355  "master",
356  url,
357  source_location,
358  };
359 
360  const std::string git_command(GIT_COMMAND);
361  if (!(QDir(source_location)).exists()) {
362  command->start(QString::fromStdString(git_command), clone_args);
363  command->waitForFinished();
364  }
365 
366  if (command->exitStatus() != QProcess::NormalExit) {
367  ERROR_MSG("Could not complete installation for module {}",
368  name.toStdString());
369  // Re-enable buttons only after compilation is done. Otherwise you get race
370  // conditions if buttons are pressed before modules are done compiling.
371  cloneButton->setEnabled(true);
372  rebuildListWidgets();
373  availableListWidget->setDisabled(false);
374  installedListWidget->setDisabled(false);
375  progress->close();
376  return;
377  }
378  // Define the commands to be run.
379  QDir package_dir = install_prefix;
380  package_dir.cdUp();
381  package_dir.cdUp();
382  const QString build_location = source_location + QString("/build");
383  const QString make_cmd = "cmake";
384  const QStringList make_config_args = {
385  "-S",
386  source_location,
387  "-B",
388  build_location,
389  QString("-DRTXI_PACKAGE_PATH=") + package_dir.path(),
390  QString("-DCMAKE_BUILD_TYPE=")
391  + QString::fromStdString(std::string(RTXI_BUILD_TYPE)),
392  QString("-DRTXI_CMAKE_SCRIPTS=")
393  + QString::fromStdString(std::string(RTXI_CMAKE_SCRIPTS))};
394  const QStringList make_build_args = {"--build", build_location, "-j2"};
395  const QStringList make_install_args = {"--install", build_location};
396 
397  progress->setLabelText("Configuring...");
398  progress->setValue(2);
399  // QApplication::processEvents();
400 
401  // Compile and install handled by QProcess.
402  command->start(make_cmd, make_config_args);
403  command->waitForFinished();
404  if (command->exitStatus() != QProcess::NormalExit || command->exitCode() != 0)
405  {
406  QMessageBox::critical(
407  nullptr,
408  "Error",
409  "Could not Configure plugin. Email help@rtxi.org for assistance");
410  const QByteArray err_str = command->readAllStandardError();
411  ERROR_MSG("{}", err_str.toStdString());
412  ERROR_MSG("Configure command for module {} failed with command {}",
413  name.toStdString(),
414  make_cmd.toStdString() + std::string(" ")
415  + make_config_args.join(" ").toStdString());
416  cloneButton->setEnabled(true);
417  rebuildListWidgets();
418  availableListWidget->setDisabled(false);
419  installedListWidget->setDisabled(false);
420  progress->close();
421  progress->close();
422  return;
423  }
424 
425  progress->setLabelText("Building...");
426  progress->setValue(3);
427  // QApplication::processEvents();
428  command->start(make_cmd, make_build_args);
429  command->waitForFinished();
430  if (command->exitStatus() != QProcess::NormalExit || command->exitCode() != 0)
431  {
432  QMessageBox::critical(
433  nullptr,
434  "Error",
435  "Could not build plugin. Email help@rtxi.org for assistance");
436  const QByteArray err_str = command->readAllStandardError();
437  ERROR_MSG("{}", err_str.toStdString());
438  ERROR_MSG("Build command for module {} failed with command {}",
439  name.toStdString(),
440  make_cmd.toStdString() + std::string(" ")
441  + make_build_args.join(" ").toStdString());
442  cloneButton->setEnabled(true);
443  rebuildListWidgets();
444  availableListWidget->setDisabled(false);
445  installedListWidget->setDisabled(false);
446  progress->close();
447  progress->close();
448  return;
449  }
450 
451  progress->setLabelText("Installing...");
452  progress->setValue(4);
453  // QApplication::processEvents();
454  command->start(make_cmd, make_install_args);
455  command->waitForFinished();
456  if (command->exitStatus() != QProcess::NormalExit || command->exitCode() != 0)
457  {
458  QMessageBox::critical(
459  nullptr,
460  "Error",
461  "Could not install plugin. Email help@rtxi.org for assistance");
462  const QByteArray err_str = command->readAllStandardError();
463  ERROR_MSG("{}", err_str.toStdString());
464  ERROR_MSG("Install command for module {} failed with command {}",
465  name.toStdString(),
466  make_cmd.toStdString() + std::string(" ")
467  + make_install_args.join(" ").toStdString());
468  cloneButton->setEnabled(true);
469  rebuildListWidgets();
470  availableListWidget->setDisabled(false);
471  installedListWidget->setDisabled(false);
472  progress->close();
473  return;
474  }
475 
476  // Add module to list of already installed modules.
477  modules[name].installed = true;
478  progress->setValue(5);
479  // QApplication::processEvents();
480  command->close();
481  progress->close();
482  cloneButton->setEnabled(true);
483  rebuildListWidgets();
484  availableListWidget->setDisabled(false);
485  installedListWidget->setDisabled(false);
486 }
487 
488 std::unique_ptr<Widgets::Plugin> RTXIWizard::createRTXIPlugin(
489  Event::Manager* ev_manager)
490 {
491  return std::make_unique<RTXIWizard::Plugin>(ev_manager);
492 }
493 
494 Widgets::Panel* RTXIWizard::createRTXIPanel(QMainWindow* main_window,
495  Event::Manager* ev_manager)
496 {
497  return static_cast<Widgets::Panel*>(
498  new RTXIWizard::Panel(main_window, ev_manager));
499 }
500 
501 std::unique_ptr<Widgets::Component> RTXIWizard::createRTXIComponent(
502  Widgets::Plugin* /*unused*/)
503 {
504  return {nullptr};
505 }
506 
508 {
513  return fact;
514 }
void installFromString(const std::string &module_name)
Panel(QMainWindow *mwindow, Event::Manager *ev_manager)
Definition: rtxi_wizard.cpp:40
QMdiSubWindow * getMdiWindow()
Definition: widgets.hpp:286
void ERROR_MSG(const std::string &errmsg, Args... args)
Definition: debug.hpp:36
constexpr std::string_view MODULE_NAME
Definition: connector.hpp:35
Widgets::FactoryMethods getFactories()
std::unique_ptr< Widgets::Plugin > createRTXIPlugin(Event::Manager *ev_manager)
Widgets::Panel * createRTXIPanel(QMainWindow *main_window, Event::Manager *ev_manager)
std::unique_ptr< Widgets::Component > createRTXIComponent(Widgets::Plugin *host_plugin)
Definition: rt.hpp:35
std::unique_ptr< Widgets::Plugin >(* createPlugin)(Event::Manager *)
Function that returns a smart pointer to plugin object.
Definition: widgets.hpp:145
Widgets::Panel *(* createPanel)(QMainWindow *, Event::Manager *)
Definition: widgets.hpp:170
std::unique_ptr< Widgets::Component >(* createComponent)(Widgets::Plugin *)
Definition: widgets.hpp:156