CustusX  2023.01.05-dev+develop.0da12
An IGT application
cxPluginFramework.cpp
Go to the documentation of this file.
1 /*=========================================================================
2 This file is part of CustusX, an Image Guided Therapy Application.
3 
4 Copyright (c) SINTEF Department of Medical Technology.
5 All rights reserved.
6 
7 CustusX is released under a BSD 3-Clause license.
8 
9 See Lisence.txt (https://github.com/SINTEFMedtek/CustusX/blob/master/License.txt) for details.
10 =========================================================================*/
11 
12 #include "cxPluginFramework.h"
13 
14 #include <QApplication>
15 #include <QStringList>
16 #include <QDirIterator>
17 #include <QFileInfo>
18 #include <QDebug>
19 
20 #include "ctkPluginFrameworkFactory.h"
21 #include "ctkPluginFramework.h"
22 #include "ctkPluginContext.h"
23 #include "ctkPluginException.h"
24 
25 #include <ctkConfig.h>
26 
27 #include "cxSettings.h"
28 #include "cxDataLocations.h"
30 
31 #include "cxFileHelpers.h"
32 #include "cxLogger.h"
33 #include <iostream>
34 #include "cxTypeConversions.h"
35 #include "cxProfile.h"
36 
37 namespace cx
38 {
39 
41 {
42  mSettingsBase = "pluginFramework";
43  mSettingsSearchPaths = mSettingsBase + "/searchPaths";
44 
45  ctkProperties fwProps;
46  QString storagePath = ProfileManager::getInstance()->getSettingsPath() + "/pluginFramework";
47 
48  fwProps[ctkPluginConstants::FRAMEWORK_STORAGE] = storagePath;
49 
50  // remove settings as stored by CTK, because full paths are stored here, causing
51  // problems when running both debug and release on the same machine (and similar).
52  fwProps[ctkPluginConstants::FRAMEWORK_STORAGE_CLEAN] = ctkPluginConstants::FRAMEWORK_STORAGE_CLEAN_ONFIRSTINIT;
53 
54  mFrameworkFactory.reset(new ctkPluginFrameworkFactory(fwProps));
55  mPluginLibFilter << "*.dll" << "*.so" << "*.dylib";
56 }
57 
59 {
60  if(mFramework->getState() == ctkPlugin::ACTIVE)//LogicManager calls stop() before the destructor is called
61  {
62  CX_LOG_CHANNEL_WARNING("plugin") << "This should not happen: PluginFrameworkManager destructor stopping plugin framework";
63  this->stop();
64  }
65 }
66 
67 QString PluginFrameworkManager::convertToRelativePath(QString path) const
68 {
69  QDir base = qApp->applicationDirPath();
70  return base.relativeFilePath(path);
71 }
72 
73 QString PluginFrameworkManager::convertToAbsolutePath(QString path) const
74 {
75  if (QDir(path).isAbsolute())
76  return QDir(path).absolutePath();
77 
78  QDir base = qApp->applicationDirPath();
79  return QDir(base.path() + "/" + path).absolutePath();
80 }
81 
82 
83 
84 std::vector<PluginFrameworkManager::PluginLoadInfo> PluginFrameworkManager::getPluginLoadInfo(QStringList symbolicNames)
85 {
86  std::vector<PluginLoadInfo> retval;
87 
88  for (unsigned i=0; i<symbolicNames.size(); ++i)
89  {
90  PluginLoadInfo info;
91  info.symbolicName = symbolicNames[i];
92  info.storedState = settings()->value(mSettingsBase+"/"+info.symbolicName).toString();
93 
94  if (info.storedState.isEmpty())
95  {
96  info.isNew = true;
97  info.storedState = getStringForctkPluginState(ctkPlugin::ACTIVE);
98  }
99  else
100  {
101  info.isNew = false;
102  }
103 
104  info.targetState = getctkPluginStateForString(info.storedState);
105 
106  retval.push_back(info);
107  }
108 
109  return retval;
110 }
111 
113 {
114  QStringList paths = settings()->value(mSettingsSearchPaths, QStringList()).toStringList();
115  this->setSearchPaths(paths);
116 
117  QStringList names = this->getPluginSymbolicNames();
118  std::vector<PluginLoadInfo> info = this->getPluginLoadInfo(names);
119 
120  // install all plugins, must do this first in order to let FW handle dependencies.
121  CX_LOG_CHANNEL_INFO("plugin") << "Installing all plugins...";
122  for (unsigned i=0; i< info.size(); ++i)
123  {
124  if (info[i].targetState != ctkPlugin::UNINSTALLED)
125  {
126  this->install(info[i].symbolicName);
127  }
128  }
129 
130  // start all plugins
131  for (unsigned i=0; i< info.size(); ++i)
132  {
133  if (info[i].targetState == ctkPlugin::ACTIVE)
134  {
135  if (info[i].isNew)
136  CX_LOG_CHANNEL_INFO("plugin") << QString("Autostarting plugin %1").arg(info[i].symbolicName);
137  else
138  CX_LOG_CHANNEL_INFO("plugin") << QString("Starting plugin %1").arg(info[i].symbolicName);
139 
140  this->start(info[i].symbolicName, ctkPlugin::START_TRANSIENT);
141  }
142  else
143  {
144  CX_LOG_CHANNEL_INFO("plugin") << QString("Set plugin to state [%2]: %1")
145  .arg(info[i].symbolicName)
146  .arg(info[i].storedState);
147  }
148  }
149 
150 }
151 
152 void PluginFrameworkManager::saveState()
153 {
154  QStringList relativePaths;
155  for (int i=0; i<mPluginSearchPaths.size(); ++i)
156  relativePaths << this->convertToRelativePath(mPluginSearchPaths[i]);
157  settings()->setValue(mSettingsSearchPaths, relativePaths);
158 
159  QStringList names = this->getPluginSymbolicNames();
160  for (unsigned i=0; i<names.size(); ++i)
161  {
162  QString name = names[i];
163  ctkPlugin::State state = this->getStateFromSymbolicName(name);
164  settings()->setValue(mSettingsBase+"/"+name, getStringForctkPluginState(state));
165  }
166 }
167 
169 {
170  ctkPlugin::State state = ctkPlugin::UNINSTALLED;
171  QSharedPointer<ctkPlugin> plugin = this->getInstalledPluginFromSymbolicName(name);
172  if (plugin)
173  state = plugin->getState();
174  return state;
175 }
176 
177 void PluginFrameworkManager::setSearchPaths(const QStringList& searchPath)
178 {
179  mPluginSearchPaths.clear();
180 
181  for (int i=0; i<searchPath.size(); ++i)
182  mPluginSearchPaths << this->convertToAbsolutePath(searchPath[i]);
183 
184  QStringList defPaths = DataLocations::getDefaultPluginsPath();
185  for (unsigned i=0; i<defPaths.size(); ++i)
186  {
187  QString defPath = this->convertToAbsolutePath(defPaths[i]);
188  if (!mPluginSearchPaths.count(defPath))
189  mPluginSearchPaths << defPath;
190  }
191 
192  mPluginSearchPaths.removeDuplicates();
193 
194  for (int i=0; i<searchPath.size(); ++i)
195  {
196  QApplication::addLibraryPath(searchPath[i]);
197  }
198  emit pluginPoolChanged();
199 }
200 
201 
203 {
204  return mPluginSearchPaths;
205 }
206 
208 {
209  return mFramework->getPluginContext();
210 }
211 
212 QSharedPointer<ctkPluginFramework> PluginFrameworkManager::getPluginFramework()
213 {
214  return mFramework;
215 }
216 
217 void PluginFrameworkManager::initializeFramework()
218 {
219  if (this->frameworkInitialized())
220  {
221  return;
222  }
223 
224  QSharedPointer<ctkPluginFramework> framework = mFrameworkFactory->getFramework();
225 
226  try
227  {
228  framework->init();
229  } catch (const ctkException& exc)
230  {
231  this->handlePluginException("Failed to initialize the plug-in framework", exc);
232  }
233  mFramework = framework;
234 }
235 
236 bool PluginFrameworkManager::frameworkInitialized() const
237 {
238  return mFramework != 0;
239 }
240 
241 bool PluginFrameworkManager::frameworkStarted() const
242 {
243  return mFramework && (mFramework->getState() == ctkPlugin::ACTIVE);
244 }
245 
246 void PluginFrameworkManager::startFramework()
247 {
248  if (!this->frameworkInitialized())
249  this->initializeFramework();
250 
251  if (this->frameworkStarted())
252  return;
253 
254  try
255  {
256  mFramework->start();
257  }
258  catch (const ctkException& exc)
259  {
260  this->handlePluginException("Failed to start the plug-in framework", exc);
261  }
262 }
263 
264 void PluginFrameworkManager::install(const QString& symbolicName)
265 {
266  this->initializeFramework();
267  if (!this->frameworkInitialized())
268  return;
269 
270  QString pluginPath = this->getPluginPath(symbolicName);
271  if (pluginPath.isEmpty())
272  return;
273 
274  try
275  {
276  ctkPluginContext* pc = this->getPluginContext();
277  pc->installPlugin(QUrl::fromLocalFile(pluginPath))->getPluginId();
278  }
279  catch (const ctkException& exc)
280  {
281  this->handlePluginException(QString("Failed to install plugin %1").arg(symbolicName), exc);
282  }
283 }
284 
286 {
287  this->startFramework();
288  return this->frameworkStarted();
289 }
290 
292 {
293  this->saveState();
294 
295  // give plugins time to clean up internal resources before different thread deletes them
296  // (obsolete because we have disabled the other-thread shutdown)
297  emit aboutToStop();
298 
299  // Bypass CTK internal 'shutdown in another thread'-mechanism, activated if we
300  // call framework::stop(). It causes too much trouble regarding Qt objects created
301  // in main thread and deleted in another thread. openCV also has trouble.
302  QStringList plugins = getPluginSymbolicNames();
303  for (int i=0; i<plugins.size(); ++i)
304  {
305  this->stop(plugins[i]);
306  }
307 
308  // stop the framework
309  try
310  {
311  mFramework->stop(); //will start a thread that destructs plugins
312  ctkPluginFrameworkEvent fe = mFramework->waitForStop(5000);
313 // int timeout = 5000;
314 // int interval = 50;
315 // ctkPluginFrameworkEvent fe;
316 // for(int i=0; i<timeout/interval; ++i)
317 // {
318 // fe = mFramework->waitForStop(interval);
319 // qApp->processEvents();
320 // }
321  if (fe.getType() == ctkPluginFrameworkEvent::FRAMEWORK_WAIT_TIMEDOUT)
322  {
323  CX_LOG_CHANNEL_WARNING("plugin") << "Stopping the plugin framework timed out";
324  return false;
325  }
326  }
327  catch (const ctkException& exc)
328  {
329  this->handlePluginException("Failed to stop the plug-in framework", exc);
330  return false;
331  }
332  return !this->frameworkStarted();
333 }
334 
335 void PluginFrameworkManager::uninstall(const QString& symbolicName)
336 {
337  QString pluginPath = getPluginPath(symbolicName);
338  if (pluginPath.isEmpty())
339  return;
340 
341  try
342  {
343  ctkPluginContext* pc = this->getPluginContext();
344  pc->installPlugin(QUrl::fromLocalFile(pluginPath))->uninstall();
345  }
346  catch (const ctkException& exc)
347  {
348  this->handlePluginException(QString("Failed to uninstall plugin %1.").arg(symbolicName), exc);
349  return;
350  }
351 
352  return;
353 }
354 
355 bool PluginFrameworkManager::start(const QString& symbolicName, ctkPlugin::StartOptions options)
356 {
357  this->startFramework();
358 
359  QString pluginPath = getPluginPath(symbolicName);
360  if (pluginPath.isEmpty())
361  {
362  CX_LOG_CHANNEL_ERROR("plugin") << QString("Failed to find plugin %1 in search path.").arg(symbolicName);
363  return false;
364  }
365 
366  try
367  {
368  ctkPluginContext* pc = this->getPluginContext();
369  QSharedPointer<ctkPlugin> plugin = pc->installPlugin(QUrl::fromLocalFile(pluginPath));
370  plugin->start(options);
371  }
372  catch (ctkException& exc)
373  {
374  this->handlePluginException(QString("Failed to stop plugin %1.").arg(symbolicName), exc);
375  return false;
376  }
377 
378  return true;
379 }
380 
381 void PluginFrameworkManager::handlePluginException(const QString& message, const ctkException& exc)
382 {
383  CX_LOG_CHANNEL_ERROR("plugin") << message;
384  const ctkException* nest = &exc;
385  while (nest)
386  {
387  CX_LOG_CHANNEL_ERROR("plugin") << QString(" cause: %1").arg(nest->what());
388  nest = nest->cause();
389  }
390 }
391 
392 bool PluginFrameworkManager::stop(const QString& symbolicName, ctkPlugin::StopOptions options)
393 {
394  if (!this->frameworkStarted())
395  return false;
396  QString pluginPath = this->getPluginPath(symbolicName);
397  if (pluginPath.isEmpty())
398  return false;
399 
400  QSharedPointer<ctkPlugin> plugin = this->getInstalledPluginFromSymbolicName(symbolicName);
401 
402  if (!plugin)
403  {
404  CX_LOG_CHANNEL_WARNING("plugin") << QString("Plugin: %1 not found").arg(symbolicName);
405  return false;
406  }
407 
408  try
409  {
410  plugin->stop(options);
411  }
412  catch (const ctkException& exc)
413  {
414  this->handlePluginException(QString("Failed to stop plugin %1").arg(symbolicName), exc);
415  return false;
416  }
417 
418  return true;
419 }
420 
421 QSharedPointer<ctkPlugin> PluginFrameworkManager::getInstalledPluginFromSymbolicName(QString symbolicName)
422 {
423  QSharedPointer<ctkPlugin> empty;
424 
425  if (!this->frameworkInitialized())
426  return empty;
427 
428  QString pluginPath = this->getPluginPath(symbolicName);
429  if (pluginPath.isEmpty())
430  return empty;
431 
432  ctkPluginContext* pc = this->getPluginContext();
433  QList < QSharedPointer<ctkPlugin> > plugins = pc->getPlugins();
434  foreach(QSharedPointer<ctkPlugin> plugin, plugins)
435  {
436  if (plugin->getSymbolicName() == symbolicName)
437  {
438  return plugin;
439  }
440  }
441 
442  return empty;
443 }
444 
445 
446 QString PluginFrameworkManager::getPluginPath(const QString& symbolicName)
447 {
448  QString pluginFileName(symbolicName);
449  pluginFileName.replace(".", "_");
450  foreach(QString searchPath, mPluginSearchPaths)
451  {
452  QDirIterator dirIter(searchPath, mPluginLibFilter, QDir::Files, QDirIterator::Subdirectories);
453  while(dirIter.hasNext())
454  {
455  dirIter.next();
456  QFileInfo fileInfo = dirIter.fileInfo();
457  QString fileBaseName = fileInfo.baseName();
458  if (fileBaseName.startsWith("lib")) fileBaseName = fileBaseName.mid(3);
459 
460  if (fileBaseName == pluginFileName)
461  {
462  return fileInfo.canonicalFilePath();
463  }
464  }
465  }
466 
467  return QString();
468 }
469 
471 {
472  QStringList result;
473  foreach(QString searchPath, mPluginSearchPaths)
474  {
475  result.append(this->getPluginSymbolicNames(searchPath));
476  }
477  result.removeDuplicates();
478  return result;
479 }
480 
481 QStringList PluginFrameworkManager::getPluginSymbolicNames(const QString& searchPath)
482 {
483  QStringList result;
484  QDirIterator dirIter(searchPath, mPluginLibFilter, QDir::Files, QDirIterator::Subdirectories);
485  while (dirIter.hasNext())
486  {
487  dirIter.next();
488  QFileInfo fileInfo = dirIter.fileInfo();
489  QString fileBaseName = fileInfo.baseName();
490  if (fileBaseName.startsWith("lib"))
491  fileBaseName = fileBaseName.mid(3);
492  QString name = fileBaseName.replace("_", ".");
493  if (this->nameIsProbablyPlugin(name))
494  result << name;
495  }
496 
497  return result;
498 }
499 
500 bool PluginFrameworkManager::nameIsProbablyPlugin(QString name) const
501 {
502  // heuristic check for plugin-ish name
503  return name.startsWith("org.custusx");
504 }
505 
506 } /* namespace cx */
#define CX_LOG_CHANNEL_INFO(channel)
Definition: cxLogger.h:108
ctkPlugin::State getStateFromSymbolicName(QString name)
#define CX_LOG_CHANNEL_WARNING(channel)
Definition: cxLogger.h:110
QStringList getSearchPaths() const
static ProfileManager * getInstance(QString defaultProfile=QString("Laboratory"))
returns the only instance of this class
Definition: cxProfile.cpp:167
QVariant value(const QString &key, const QVariant &defaultValue=QVariant()) const
Definition: cxSettings.cpp:66
QSharedPointer< ctkPluginFramework > getPluginFramework()
void uninstall(const QString &symbolicName)
void install(const QString &symbolicName)
QSharedPointer< ctkPlugin > getInstalledPluginFromSymbolicName(QString symbolicName)
void setValue(const QString &key, const QVariant &value)
Definition: cxSettings.cpp:58
ctkPluginContext * getPluginContext()
Settings * settings()
Shortcut for accessing the settings instance.
Definition: cxSettings.cpp:21
static QStringList getDefaultPluginsPath()
return the folder where plugins should be located, by default.
QString getStringForctkPluginState(const ctkPlugin::State state)
QString getSettingsPath()
Definition: cxProfile.cpp:220
ctkPlugin::State getctkPluginStateForString(QString text)
#define CX_LOG_CHANNEL_ERROR(channel)
Definition: cxLogger.h:111
void setSearchPaths(const QStringList &searchPath)
Namespace for all CustusX production code.