pluginmanager.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483
  1. /*
  2. The code is adpted from QtNote project
  3. Copyright (C) 2010-2016 Sergey Ili'nykh
  4. This program is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with this program. If not, see <http://www.gnu.org/licenses/>.
  14. Contacts:
  15. E-Mail: rion4ik@gmail.com XMPP: rion@jabber.ru
  16. */
  17. /*
  18. Algo for plugins search.
  19. ------------------------
  20. Plugins priority:
  21. 1) Directories for plugin search have priority
  22. To choose between plugins with same ID.
  23. AppInfo::appPluginsDirs() return directories in priority order
  24. (most prioretized first)
  25. If there are two plugins with the same id in some directory,
  26. the one with more recent modification time will be chosen.
  27. 2) Plugins have load priority
  28. To decide which plugin to load first.
  29. We have few different states of plugins
  30. 1) Discovered plugins:
  31. All files which look like suitable plugins.
  32. In this case for example we can have 2 or more copies of the same
  33. plugin and only one of them should become available.
  34. All of this data will be cached.
  35. 2) Available plugins
  36. Plugins ready to be loaded. All of them have unique ID.
  37. All not suitable dups by prioriry or other means are removed.
  38. This list of plugins could be available to other code as
  39. priority sorted list.
  40. 3) Enabled plugins
  41. Subset of available plugins which are supposed to be loaded.
  42. 4) Loaded plugins
  43. Successfully loaded plugins
  44. Compilation mode:
  45. AppInfo::appPluginsDirs() can return different set of directories
  46. depending on compilation options
  47. 1) DEV
  48. This could be set as qmake param. It's developer's mode.
  49. In this mode plugins search works differently. Only just compiled
  50. plugins' directories added to search, so only they will be loaded.
  51. 2) Normal mode
  52. In normal mode we have two directories for search.
  53. One in ~/local/share/QStarDict/plugins and another one is
  54. /usr/lib/QStarDict/plugins.
  55. The directories could have different paths dependeing on
  56. user and system settings.
  57. Algo:
  58. 1) Iterate over all plugins' directories and find all suitable plugins.
  59. 2) Cache this data in order they were found
  60. 3) Iterate over cache and make a list of "available" plugins.
  61. In other words make do not add tot the list plugins with already
  62. known ID from previous iterations.
  63. 4) Sort "available" list according to priority set by user.
  64. 5) Save the list to settings for future use.
  65. 6) Iterate over sorted "available" list check which plugin is enabled
  66. and load it.
  67. 7) emit "pluginLoaded" signal for each loaded plugin.
  68. Business rules:
  69. 1) Each plugin should know its priority as was discovered.
  70. 2) Plugins are not expected to heavily iteract with application
  71. on load, since we can load a plugin just to update metadata.
  72. */
  73. #include <QSettings>
  74. #include <QStringList>
  75. #include <QDir>
  76. #include <QPluginLoader>
  77. #include <QSet>
  78. #include <QApplication>
  79. # include <QJsonArray>
  80. #include <QDebug>
  81. #include "appinfo.h"
  82. #include "pluginmanager.h"
  83. #include "../plugins/pluginserver.h"
  84. namespace QStarDict {
  85. class PluginServerImpl : public QObject, public PluginServer
  86. {
  87. Q_OBJECT
  88. Q_INTERFACES(QStarDict::PluginServer)
  89. PluginManager *pm;
  90. public:
  91. PluginServerImpl(PluginManager *parent) :
  92. QObject(parent),
  93. pm(parent)
  94. {
  95. }
  96. const PluginMetadata &metadata(const QString &pluginId) const
  97. {
  98. return pm->pluginDesc(pluginId)->metadata;
  99. }
  100. /**
  101. * Return a directory that contains plugin's data.
  102. */
  103. QString configDir(const QString &pluginId) const
  104. {
  105. QString path = AppInfo::configDir() + QLatin1String("/pluginsdata/") + metadata(pluginId).name;
  106. if (! QDir::root().exists(path))
  107. QDir::root().mkpath(path);
  108. return path;
  109. }
  110. QString idForPlugin(QObject *instance) const
  111. {
  112. auto data = pm->findPluginInstance(instance);
  113. if (data) {
  114. return data->metadata.id;
  115. }
  116. return QString();
  117. }
  118. };
  119. class PluginsIterator
  120. {
  121. QDir currentDir;
  122. QStringList pluginsDirs;
  123. QStringList plugins;
  124. QStringList::const_iterator pluginsDirsIt;
  125. QStringList::const_iterator pluginsIt;
  126. public:
  127. PluginsIterator()
  128. {
  129. pluginsDirs = AppInfo::appPluginsDirs();
  130. pluginsDirsIt = pluginsDirs.constBegin();
  131. findDirWithPlugins();
  132. }
  133. void next()
  134. {
  135. if (pluginsIt != plugins.constEnd()) {
  136. pluginsIt++;
  137. }
  138. if (pluginsIt == plugins.constEnd() && pluginsDirsIt != pluginsDirs.constEnd()) {
  139. pluginsDirsIt++;
  140. findDirWithPlugins();
  141. }
  142. }
  143. inline bool isFinished() const { return pluginsIt == plugins.constEnd() &&
  144. pluginsDirsIt == pluginsDirs.constEnd(); }
  145. inline QString fileName() const
  146. {
  147. if (pluginsIt != plugins.constEnd()) {
  148. return QFileInfo(currentDir.filePath(*pluginsIt)).canonicalFilePath();
  149. }
  150. return QString();
  151. }
  152. private:
  153. void findDirWithPlugins()
  154. {
  155. while (pluginsDirsIt != pluginsDirs.constEnd()) {
  156. currentDir = QDir(*pluginsDirsIt);
  157. if (currentDir.isReadable()) {
  158. plugins.clear();
  159. foreach(const QString &file, currentDir.entryList(QDir::Files, QDir::Time)) {
  160. if (QLibrary::isLibrary(file)) {
  161. plugins.append(file);
  162. }
  163. }
  164. if (!plugins.isEmpty()) {
  165. pluginsIt = plugins.constBegin();
  166. return;
  167. }
  168. }
  169. pluginsDirsIt++;
  170. }
  171. pluginsIt = plugins.constEnd();
  172. }
  173. };
  174. PluginManager::PluginManager()
  175. {
  176. pluginServer = new PluginServerImpl(this);
  177. QDir(iconsCacheDir()).mkpath(QLatin1String("."));
  178. updateMetadata();
  179. }
  180. PluginManager::~PluginManager()
  181. {
  182. QHashIterator<QString, Plugin::Ptr> it(plugins);
  183. while (it.hasNext()) {
  184. auto item = it.next();
  185. if (item.value()->isLoaded()) {
  186. item.value()->unload();
  187. }
  188. }
  189. }
  190. void PluginManager::loadPlugins()
  191. {
  192. QSettings s;
  193. QStringList prioritizedList = s.value("plugins-priority").toStringList();
  194. QMutableStringListIterator it(prioritizedList);
  195. while (it.hasNext()) {
  196. QString id = it.next();
  197. if (id.isEmpty() || !plugins.contains(id)) {
  198. it.remove();
  199. }
  200. }
  201. // now prioritizedList comprises only known plugin
  202. // and any not previously prioritized
  203. QStringList notPrioritizedYet = (plugins.keys().toSet() - prioritizedList.toSet()).toList();
  204. notPrioritizedYet.sort();
  205. prioritizedList += notPrioritizedYet;
  206. s.setValue("plugins-priority", prioritizedList);
  207. /*
  208. * now we have fully prioritized list.
  209. */
  210. QStringListIterator it2(prioritizedList);
  211. while (it2.hasNext()) {
  212. Plugin::Ptr pd = plugins[it2.next()];
  213. auto state = s.value("plugins/" + pd->metadata.id + "/state", pd->state).toUInt();
  214. pd->state = state;
  215. if (!pd->isEnabled() || pd->isLoaded()) {
  216. continue;
  217. }
  218. pd->load();
  219. }
  220. }
  221. QString PluginManager::iconsCacheDir()
  222. {
  223. return AppInfo::cacheDir() + QLatin1String("/plugin-icons");
  224. }
  225. void PluginManager::setEnabled(const QString &pluginId, bool enabled)
  226. {
  227. QSettings s;
  228. auto pd = plugins.value(pluginId);
  229. if (!pd) {
  230. return;
  231. }
  232. pd->setEnabled(enabled);
  233. s.beginGroup("plugins");
  234. s.beginGroup(pluginId);
  235. s.setValue("state", (int)pd->state);
  236. if (enabled) {
  237. if (!pd->isLoaded()) {
  238. if (pd->load() != LoadError::NoError) {
  239. emit pluginLoaded(pd->metadata.id);
  240. }
  241. }
  242. } else if (pd->isLoaded()) {
  243. if (!pd->unload()) {
  244. qWarning("Failed to unload %s: %s", qPrintable(pd->loader->fileName()), qPrintable(pd->loader->errorString()));
  245. }
  246. }
  247. }
  248. QStringList PluginManager::availablePlugins() const
  249. {
  250. return QSettings().value("plugins-priority").toStringList();
  251. }
  252. QStringList PluginManager::loadedPlugins() const
  253. {
  254. QStringList ret;
  255. foreach (const QString &pluginId, plugins.keys()) {
  256. auto ptr = plugins.value(pluginId);
  257. if (ptr->isLoaded()) {
  258. ret.append(pluginId);
  259. }
  260. }
  261. return ret;
  262. }
  263. void PluginManager::setLoadedPlugins(const QStringList &pluginIds)
  264. {
  265. QSet<QString> ps = QSet<QString>::fromList(pluginIds);
  266. QMutableHashIterator<QString, Plugin::Ptr> i(plugins);
  267. while(i.hasNext()) {
  268. auto p = i.next();
  269. setEnabled(p.key(), ps.contains(p.key()));
  270. }
  271. }
  272. PluginManager::Plugin::Ptr PluginManager::findPluginInstance(QObject *instance) const
  273. {
  274. foreach (const QString &pluginId, plugins.keys()) {
  275. auto ptr = plugins.value(pluginId);
  276. if (ptr->isLoaded() && ptr->loader->instance() == instance) {
  277. return ptr;
  278. }
  279. }
  280. return PluginManager::Plugin::Ptr();
  281. }
  282. void PluginManager::updateMetadata()
  283. {
  284. QSettings s;
  285. QSettings metaCache(AppInfo::cacheDir() + QLatin1String("/pluginsmeta.ini"), QSettings::IniFormat);
  286. //decltype(plugins) tmpPlugins;
  287. QHash<QString, Plugin::Ptr> file2data;
  288. // read metadata cache
  289. int size = metaCache.beginReadArray("list");
  290. for (int i = 0; i < size; ++i) {
  291. metaCache.setArrayIndex(i);
  292. QString fileName = metaCache.value("filename", QString()).toString();
  293. if (fileName.isEmpty()) {
  294. break;
  295. }
  296. Plugin::Ptr pd(new Plugin);
  297. //tmpPlugins[pluginId] = pd;
  298. file2data[fileName] = pd;
  299. pd->state = metaCache.value("state").toUInt();
  300. pd->state &= ~Plugin::Exist; // reset it until we sure it exists
  301. pd->modifyTime = QDateTime::fromTime_t(metaCache.value("lastModify").toUInt()); // if 0 then we have staled cache. it's ok
  302. pd->metadata.id = metaCache.value("id").toString();
  303. pd->metadata.name = metaCache.value("name").toString();
  304. pd->metadata.description = metaCache.value("description").toString();
  305. pd->metadata.authors = metaCache.value("authors").toStringList();
  306. pd->metadata.features = metaCache.value("features").toStringList();
  307. pd->metadata.version = metaCache.value("version").toUInt();
  308. pd->metadata.extra = metaCache.value("extra").toMap();
  309. pd->metadata.icon = QIcon(iconsCacheDir() + '/' + pd->metadata.id + QLatin1String(".png"));
  310. //pd->metadata.extra = s.value("extra").();
  311. }
  312. metaCache.endArray();
  313. lastError = LoadError::NoError;
  314. metaCache.beginWriteArray("list");
  315. int cacheIndex = 0;
  316. PluginsIterator it;
  317. while (!it.isFinished()) {
  318. metaCache.setArrayIndex(cacheIndex);
  319. QString fileName = it.fileName();
  320. it.next();
  321. Plugin::Ptr pd = file2data.value(fileName);
  322. bool isnew = pd.isNull();
  323. if (isnew) {
  324. pd = Plugin::Ptr(new Plugin);
  325. pd->state |= Plugin::Exist;
  326. }
  327. pd->pluginServer = pluginServer;
  328. pd->loader = new QPluginLoader(fileName);
  329. if (isnew || (!pd->isLoaded() &&
  330. pd->modifyTime != QFileInfo(pd->loader->fileName()).lastModified())) { // have to update metadata cache
  331. pd->modifyTime = QFileInfo(fileName).lastModified();
  332. auto js = pd->loader->metaData().value(QLatin1String("MetaData")).toObject();
  333. QString id = js.value(QLatin1String("id")).toString();
  334. QString name = js.value(QLatin1String("name")).toString();
  335. if (id.isEmpty() || name.isEmpty()) {
  336. pd->unload();
  337. qDebug("QStarDict plugin %s did not set metadata id or name. ignore it", qPrintable(fileName));
  338. lastError = LoadError::Metadata;
  339. pd->state &= ~(Plugin::Valid | Plugin::Enabled); // mark invalid and disable
  340. continue;
  341. }
  342. pd->metadata.id = id;
  343. pd->metadata.name = name;
  344. pd->metadata.description = js.value(QLatin1String("description")).toString();
  345. pd->metadata.authors = js.value(QLatin1String("authors")).toString().split(';');
  346. pd->metadata.features = js.value(QLatin1String("features")).toString().split(';');
  347. pd->metadata.version = js.value(QLatin1String("version")).toString();
  348. // extra?
  349. if (isnew) {
  350. pd->setEnabled(pd->metadata.features.contains("defenable"));
  351. }
  352. if (!pd->isEnabled()) {
  353. pd->unload();
  354. }
  355. pd->state |= Plugin::Valid;
  356. metaCache.setValue("id", pd->metadata.id);
  357. metaCache.setValue("state", (int)pd->state);
  358. metaCache.setValue("filename", pd->loader->fileName());
  359. metaCache.setValue("lastModify", pd->modifyTime.toTime_t());
  360. metaCache.setValue("name", pd->metadata.name);
  361. metaCache.setValue("description", pd->metadata.description);
  362. metaCache.setValue("authors", pd->metadata.authors);
  363. metaCache.setValue("features", pd->metadata.features);
  364. metaCache.setValue("version", pd->metadata.version);
  365. metaCache.setValue("extra", pd->metadata.extra);
  366. cacheIndex++;
  367. }
  368. plugins.insert(pd->metadata.id, pd);
  369. }
  370. metaCache.endArray();
  371. }
  372. void PluginManager::Plugin::cacheIcon()
  373. {
  374. if (!metadata.icon.isNull()) {
  375. QFileInfo fi(PluginManager::iconsCacheDir() + '/' + metadata.id + QLatin1String(".png"));
  376. if (!fi.exists() || fi.lastModified() < modifyTime) {
  377. metadata.icon.pixmap(16, 16).save(fi.filePath());
  378. }
  379. }
  380. }
  381. PluginManager::LoadError PluginManager::Plugin::load()
  382. {
  383. if (isLoaded()) {
  384. return LoadError::NoError;
  385. }
  386. #ifdef DEV
  387. qDebug("Loading plugin: %s", qPrintable(loader->fileName()));
  388. #endif
  389. QObject *plugin = loader->instance();
  390. if (!plugin) {
  391. qDebug("failed to load %s : %s", qPrintable(loader->fileName()), qPrintable(loader->errorString()));
  392. return LoadError::NotPlugin;
  393. }
  394. BasePlugin *qnp = qobject_cast<BasePlugin *>(plugin);
  395. if (!qnp) {
  396. loader->unload();
  397. qDebug("not QStarDict plugin %s. ignore it", qPrintable(loader->fileName()));
  398. return LoadError::ABI;
  399. }
  400. qnp->qsd = pluginServer;
  401. metadata.icon = qnp->pluginIcon();
  402. cacheIcon();
  403. return LoadError::NoError;
  404. }
  405. bool PluginManager::Plugin::unload()
  406. {
  407. QString fileName = loader->fileName();
  408. if (loader->unload()) {
  409. delete loader;
  410. // probably Qt bug but "instance" method doesn't work after unload. So recreate loader.
  411. loader = new QPluginLoader(fileName);
  412. return true;
  413. }
  414. qWarning("Failed to unload plugin: %s", qPrintable(metadata.name));
  415. return false;
  416. }
  417. } // namespace QtNote
  418. #include "pluginmanager.moc"