You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PluginManager.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602
  1. /*
  2. * Copyright (c) 2006-2014 DMDirc Developers
  3. *
  4. * Permission is hereby granted, free of charge, to any person obtaining a copy
  5. * of this software and associated documentation files (the "Software"), to deal
  6. * in the Software without restriction, including without limitation the rights
  7. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. * copies of the Software, and to permit persons to whom the Software is
  9. * furnished to do so, subject to the following conditions:
  10. *
  11. * The above copyright notice and this permission notice shall be included in
  12. * all copies or substantial portions of the Software.
  13. *
  14. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  15. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  16. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  17. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  18. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  19. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  20. * SOFTWARE.
  21. */
  22. package com.dmdirc.plugins;
  23. import com.dmdirc.DMDircMBassador;
  24. import com.dmdirc.events.AppErrorEvent;
  25. import com.dmdirc.events.ClientPrefsClosedEvent;
  26. import com.dmdirc.events.ClientPrefsOpenedEvent;
  27. import com.dmdirc.events.PluginRefreshEvent;
  28. import com.dmdirc.events.UserErrorEvent;
  29. import com.dmdirc.interfaces.config.IdentityController;
  30. import com.dmdirc.logger.ErrorLevel;
  31. import com.dmdirc.updater.components.PluginComponent;
  32. import com.dmdirc.updater.manager.UpdateManager;
  33. import com.dmdirc.util.collections.MapList;
  34. import java.io.File;
  35. import java.net.MalformedURLException;
  36. import java.net.URL;
  37. import java.util.ArrayList;
  38. import java.util.Arrays;
  39. import java.util.Collection;
  40. import java.util.Deque;
  41. import java.util.HashMap;
  42. import java.util.HashSet;
  43. import java.util.LinkedList;
  44. import java.util.List;
  45. import java.util.Map;
  46. import javax.inject.Provider;
  47. import dagger.ObjectGraph;
  48. import net.engio.mbassy.listener.Handler;
  49. /**
  50. * Searches for and manages plugins and services.
  51. */
  52. public class PluginManager implements ServiceManager {
  53. /** List of known plugins' file names to their corresponding {@link PluginInfo} objects. */
  54. private final Map<String, PluginInfo> knownPlugins = new HashMap<>();
  55. /** Set of known plugins' metadata. */
  56. private final Collection<PluginMetaData> plugins = new HashSet<>();
  57. /** Directory where plugins are stored. */
  58. private final String directory;
  59. /** The identity controller to use to find configuration options. */
  60. private final IdentityController identityController;
  61. /** The update manager to inform about plugins. */
  62. private final UpdateManager updateManager;
  63. /** A provider of initialisers for plugin injectors. */
  64. private final Provider<PluginInjectorInitialiser> initialiserProvider;
  65. /** Map of services. */
  66. private final Map<String, Map<String, Service>> services = new HashMap<>();
  67. /** Global ClassLoader used by plugins from this manager. */
  68. private final GlobalClassLoader globalClassLoader;
  69. /** The graph to pass to plugins for DI purposes. */
  70. private final ObjectGraph objectGraph;
  71. /** Event bus to pass to plugin info for plugin loaded events. */
  72. private final DMDircMBassador eventBus;
  73. /**
  74. * Creates a new instance of PluginManager.
  75. *
  76. * @param eventBus The event bus to subscribe to events on
  77. * @param identityController The identity controller to use for configuration options.
  78. * @param updateManager The update manager to inform about plugins.
  79. * @param initialiserProvider A provider of initialisers for plugin injectors.
  80. * @param objectGraph The graph to pass to plugins for DI purposes.
  81. * @param directory The directory to load plugins from.
  82. */
  83. public PluginManager(
  84. final DMDircMBassador eventBus,
  85. final IdentityController identityController,
  86. final UpdateManager updateManager,
  87. final Provider<PluginInjectorInitialiser> initialiserProvider,
  88. final ObjectGraph objectGraph,
  89. final String directory) {
  90. this.identityController = identityController;
  91. this.updateManager = updateManager;
  92. this.initialiserProvider = initialiserProvider;
  93. this.directory = directory;
  94. this.globalClassLoader = new GlobalClassLoader(this);
  95. this.objectGraph = objectGraph;
  96. this.eventBus = eventBus;
  97. eventBus.subscribe(this);
  98. }
  99. /**
  100. * Get the global class loader in use for this plugin manager.
  101. *
  102. * @return Global Class Loader
  103. */
  104. public GlobalClassLoader getGlobalClassLoader() {
  105. return globalClassLoader;
  106. }
  107. /** {@inheritDoc} */
  108. @Override
  109. public Service getService(final String type, final String name) {
  110. return getService(type, name, false);
  111. }
  112. /** {@inheritDoc} */
  113. @Override
  114. public Service getService(final String type, final String name, final boolean create) {
  115. // Find the type first
  116. if (services.containsKey(type)) {
  117. final Map<String, Service> map = services.get(type);
  118. // Now the name
  119. if (map.containsKey(name)) {
  120. return map.get(name);
  121. } else if (create) {
  122. final Service service = new Service(type, name);
  123. map.put(name, service);
  124. return service;
  125. }
  126. } else if (create) {
  127. final Map<String, Service> map = new HashMap<>();
  128. final Service service = new Service(type, name);
  129. map.put(name, service);
  130. services.put(type, map);
  131. return service;
  132. }
  133. return null;
  134. }
  135. /** {@inheritDoc} */
  136. @Override
  137. public ServiceProvider getServiceProvider(final String type, final String name) throws
  138. NoSuchProviderException {
  139. final Service service = getService(type, name);
  140. if (service != null) {
  141. ServiceProvider provider = service.getActiveProvider();
  142. if (provider != null) {
  143. return provider;
  144. } else {
  145. // Try to activate the service then try again.
  146. service.activate();
  147. provider = service.getActiveProvider();
  148. if (provider != null) {
  149. return provider;
  150. }
  151. }
  152. }
  153. throw new NoSuchProviderException("No provider found for: " + type + "->" + name);
  154. }
  155. /** {@inheritDoc} */
  156. @Override
  157. public ServiceProvider getServiceProvider(final String type, final List<String> names,
  158. final boolean fallback) throws NoSuchProviderException {
  159. for (final String name : names) {
  160. final ServiceProvider provider = getServiceProvider(type, name);
  161. if (provider != null) {
  162. return provider;
  163. }
  164. }
  165. if (fallback) {
  166. final List<Service> servicesType = getServicesByType(type);
  167. if (!servicesType.isEmpty()) {
  168. final Service service = servicesType.get(0);
  169. return getServiceProvider(type, service.getName());
  170. }
  171. }
  172. throw new NoSuchProviderException("No provider found for " + type + "from the given list");
  173. }
  174. /** {@inheritDoc} */
  175. @Override
  176. public ExportedService getExportedService(final String name) {
  177. return getServiceProvider("export", name).getExportedService(name);
  178. }
  179. /** {@inheritDoc} */
  180. @Override
  181. public List<Service> getServicesByType(final String type) {
  182. // Find the type first
  183. if (services.containsKey(type)) {
  184. final Map<String, Service> map = services.get(type);
  185. return new ArrayList<>(map.values());
  186. }
  187. return new ArrayList<>();
  188. }
  189. /** {@inheritDoc} */
  190. @Override
  191. public List<Service> getAllServices() {
  192. // Find the type first
  193. final List<Service> allServices = new ArrayList<>();
  194. for (Map<String, Service> map : services.values()) {
  195. allServices.addAll(map.values());
  196. }
  197. return allServices;
  198. }
  199. /**
  200. * Autoloads plugins.
  201. */
  202. public void doAutoLoad() {
  203. for (String plugin : identityController.getGlobalConfiguration().getOptionList("plugins",
  204. "autoload")) {
  205. plugin = plugin.trim();
  206. if (!plugin.isEmpty() && plugin.charAt(0) != '#' && getPluginInfo(plugin) != null) {
  207. getPluginInfo(plugin).loadPlugin();
  208. }
  209. }
  210. }
  211. /**
  212. * Tests and adds the specified plugin to the known plugins list. Plugins will only be added if:
  213. * <ul><li>The file exists,<li>No other plugin with the same name is known,<li>All requirements
  214. * are met for the plugin,
  215. * <li>The plugin has a valid config file that can be read</ul>.
  216. *
  217. * @param filename Filename of Plugin jar
  218. *
  219. * @return True if the plugin is in the known plugins list (either before this invocation or as
  220. * a result of it), false if it was not added for one of the reasons outlined above.
  221. */
  222. public boolean addPlugin(final String filename) {
  223. if (knownPlugins.containsKey(filename.toLowerCase())) {
  224. return true;
  225. }
  226. if (!new File(directory, filename).exists()) {
  227. eventBus.publish(new UserErrorEvent(ErrorLevel.MEDIUM, null,
  228. "Error loading plugin " + filename + ": File does not exist", ""));
  229. return false;
  230. }
  231. try {
  232. final PluginMetaData metadata = new PluginMetaData(this,
  233. new URL("jar:file:" + directory + filename
  234. + "!/META-INF/plugin.config"),
  235. new URL("file:" + directory + filename));
  236. metadata.load();
  237. final PluginInfo pluginInfo = new PluginInfo(metadata, initialiserProvider, eventBus,
  238. identityController, objectGraph);
  239. final PluginInfo existing = getPluginInfoByName(metadata.getName());
  240. if (existing != null) {
  241. eventBus.publish(new UserErrorEvent(ErrorLevel.MEDIUM, null,
  242. "Duplicate Plugin detected, Ignoring. (" + filename
  243. + " is the same as " + existing.getFilename() + ")", ""));
  244. return false;
  245. }
  246. if ((metadata.getUpdaterId() > 0 && metadata.getVersion().isValid())
  247. || (identityController.getGlobalConfiguration()
  248. .hasOptionInt("plugin-addonid", metadata.getName()))) {
  249. updateManager.addComponent(new PluginComponent(
  250. identityController.getGlobalConfiguration(), pluginInfo));
  251. }
  252. knownPlugins.put(filename.toLowerCase(), pluginInfo);
  253. eventBus.publishAsync(new PluginRefreshEvent());
  254. return true;
  255. } catch (MalformedURLException mue) {
  256. eventBus.publish(new UserErrorEvent(ErrorLevel.MEDIUM, mue,
  257. "Error creating URL for plugin " + filename + ": " + mue.getMessage(), ""));
  258. } catch (PluginException e) {
  259. eventBus.publish(new UserErrorEvent(ErrorLevel.MEDIUM, e,
  260. "Error loading plugin " + filename + ": " + e.getMessage(), ""));
  261. }
  262. return false;
  263. }
  264. /**
  265. * Remove a plugin.
  266. *
  267. * @param filename Filename of Plugin jar
  268. *
  269. * @return True if removed.
  270. */
  271. public boolean delPlugin(final String filename) {
  272. if (!knownPlugins.containsKey(filename.toLowerCase())) {
  273. return false;
  274. }
  275. final PluginInfo pluginInfo = getPluginInfo(filename);
  276. final boolean wasLoaded = pluginInfo.isLoaded();
  277. if (wasLoaded && !pluginInfo.isUnloadable()) {
  278. return false;
  279. }
  280. pluginInfo.unloadPlugin();
  281. knownPlugins.remove(filename.toLowerCase());
  282. return true;
  283. }
  284. /**
  285. * Reload a plugin.
  286. *
  287. * @param filename Filename of Plugin jar
  288. *
  289. * @return True if reloaded.
  290. */
  291. public boolean reloadPlugin(final String filename) {
  292. if (!knownPlugins.containsKey(filename.toLowerCase())) {
  293. return false;
  294. }
  295. final PluginInfo pluginInfo = getPluginInfo(filename);
  296. final boolean wasLoaded = pluginInfo.isLoaded();
  297. if (wasLoaded && !pluginInfo.isUnloadable()) {
  298. return false;
  299. }
  300. delPlugin(filename);
  301. boolean result = addPlugin(filename);
  302. if (wasLoaded && result) {
  303. getPluginInfo(filename).loadPlugin();
  304. result = getPluginInfo(filename).isLoaded();
  305. }
  306. return result;
  307. }
  308. /**
  309. * Reload all plugins.
  310. */
  311. public void reloadAllPlugins() {
  312. for (PluginInfo pluginInfo : getPluginInfos()) {
  313. reloadPlugin(pluginInfo.getFilename());
  314. }
  315. }
  316. /**
  317. * Get a plugin instance.
  318. *
  319. * @param filename File name of plugin jar
  320. *
  321. * @return PluginInfo instance, or null
  322. */
  323. public PluginInfo getPluginInfo(final String filename) {
  324. return knownPlugins.get(filename.toLowerCase());
  325. }
  326. /**
  327. * Get a plugin instance by plugin name.
  328. *
  329. * @param name Name of plugin to find.
  330. *
  331. * @return PluginInfo instance, or null
  332. */
  333. public PluginInfo getPluginInfoByName(final String name) {
  334. for (PluginInfo pluginInfo : getPluginInfos()) {
  335. if (pluginInfo.getMetaData().getName().equalsIgnoreCase(name)) {
  336. return pluginInfo;
  337. }
  338. }
  339. return null;
  340. }
  341. /**
  342. * Get directory where plugins are stored.
  343. *
  344. * @return Directory where plugins are stored.
  345. *
  346. * @deprecated Should be injected.
  347. */
  348. @Deprecated
  349. public String getDirectory() {
  350. return directory;
  351. }
  352. /**
  353. * Get directory where plugin files are stored.
  354. *
  355. * @return Directory where plugin files are stored.
  356. */
  357. public String getFilesDirectory() {
  358. final String fs = System.getProperty("file.separator");
  359. String filesDir = directory + "files" + fs;
  360. if (identityController.getGlobalConfiguration().hasOptionString("plugins", "filesdir")) {
  361. final String fdopt = identityController.getGlobalConfiguration()
  362. .getOptionString("plugins", "filesdir");
  363. if (fdopt != null && !fdopt.isEmpty() && new File(fdopt).exists()) {
  364. filesDir = fdopt;
  365. }
  366. }
  367. return filesDir;
  368. }
  369. /**
  370. * Refreshes the list of known plugins.
  371. */
  372. public void refreshPlugins() {
  373. applyUpdates();
  374. final Collection<PluginMetaData> newPlugins = getAllPlugins();
  375. for (PluginMetaData plugin : newPlugins) {
  376. addPlugin(plugin.getRelativeFilename());
  377. }
  378. // Update our list of plugins
  379. synchronized (plugins) {
  380. plugins.removeAll(newPlugins);
  381. for (PluginMetaData oldPlugin : new HashSet<>(plugins)) {
  382. delPlugin(oldPlugin.getRelativeFilename());
  383. }
  384. plugins.clear();
  385. plugins.addAll(newPlugins);
  386. }
  387. eventBus.publishAsync(new PluginRefreshEvent());
  388. }
  389. /**
  390. * Recursively scans the plugin directory and attempts to apply any available updates.
  391. */
  392. public void applyUpdates() {
  393. final Deque<File> dirs = new LinkedList<>();
  394. dirs.add(new File(directory));
  395. while (!dirs.isEmpty()) {
  396. final File dir = dirs.pop();
  397. if (dir.isDirectory()) {
  398. dirs.addAll(Arrays.asList(dir.listFiles()));
  399. } else if (dir.isFile() && dir.getName().endsWith(".jar")) {
  400. final File update = new File(dir.getAbsolutePath() + ".update");
  401. if (update.exists() && dir.delete()) {
  402. update.renameTo(dir);
  403. }
  404. }
  405. }
  406. }
  407. /**
  408. * Retrieves a list of all installed plugins. Any file under the main plugin directory
  409. * (~/.DMDirc/plugins or similar) that matches *.jar is deemed to be a valid plugin.
  410. *
  411. * @return A list of all installed or known plugins
  412. */
  413. public Collection<PluginMetaData> getAllPlugins() {
  414. final Collection<PluginMetaData> res = new HashSet<>(plugins.size());
  415. final Deque<File> dirs = new LinkedList<>();
  416. final Collection<String> pluginPaths = new LinkedList<>();
  417. dirs.add(new File(directory));
  418. while (!dirs.isEmpty()) {
  419. final File dir = dirs.pop();
  420. if (dir.isDirectory()) {
  421. dirs.addAll(Arrays.asList(dir.listFiles()));
  422. } else if (dir.isFile() && dir.getName().endsWith(".jar")) {
  423. pluginPaths.add(dir.getPath().substring(directory.length()));
  424. }
  425. }
  426. final MapList<String, String> newServices = new MapList<>();
  427. final Map<String, PluginMetaData> newPluginsByName = new HashMap<>();
  428. final Map<String, PluginMetaData> newPluginsByPath = new HashMap<>();
  429. // Initialise all of our metadata objects
  430. for (String target : pluginPaths) {
  431. try {
  432. final PluginMetaData targetMetaData = new PluginMetaData(this,
  433. new URL("jar:file:" + directory + target
  434. + "!/META-INF/plugin.config"),
  435. new URL("file:" + directory + target));
  436. targetMetaData.load();
  437. if (targetMetaData.hasErrors()) {
  438. eventBus.publish(new UserErrorEvent(ErrorLevel.MEDIUM, null,
  439. "Error reading plugin metadata for plugin " + target
  440. + ": " + targetMetaData.getErrors(), ""));
  441. } else {
  442. newPluginsByName.put(targetMetaData.getName(), targetMetaData);
  443. newPluginsByPath.put(target, targetMetaData);
  444. for (String service : targetMetaData.getServices()) {
  445. final String[] parts = service.split(" ", 2);
  446. newServices.add(parts[1], parts[0]);
  447. }
  448. for (String export : targetMetaData.getExports()) {
  449. final String[] parts = export.split(" ");
  450. final String name = parts.length > 4 ? parts[4] : parts[0];
  451. newServices.add("export", name);
  452. }
  453. }
  454. } catch (MalformedURLException mue) {
  455. eventBus.publish(new UserErrorEvent(ErrorLevel.MEDIUM, mue,
  456. "Error creating URL for plugin " + target + ": " + mue.getMessage(), ""));
  457. }
  458. }
  459. // Now validate all of the plugins
  460. for (Map.Entry<String, PluginMetaData> target : newPluginsByPath.entrySet()) {
  461. final PluginMetaDataValidator validator = new PluginMetaDataValidator(target.getValue());
  462. final Collection<String> results = validator.validate(newPluginsByName, newServices);
  463. if (results.isEmpty()) {
  464. res.add(target.getValue());
  465. } else {
  466. eventBus.publish(new UserErrorEvent(ErrorLevel.MEDIUM, null,
  467. "Plugin validation failed for " + target.getKey() + ": " + results, ""));
  468. }
  469. }
  470. return res;
  471. }
  472. /**
  473. * Update the autoLoadList
  474. *
  475. * @param plugin to add/remove (Decided automatically based on isLoaded())
  476. */
  477. public void updateAutoLoad(final PluginInfo plugin) {
  478. final List<String> list = identityController.getGlobalConfiguration()
  479. .getOptionList("plugins", "autoload");
  480. final String path = plugin.getMetaData().getRelativeFilename();
  481. if (plugin.isLoaded() && !list.contains(path)) {
  482. list.add(path);
  483. } else if (!plugin.isLoaded() && list.contains(path)) {
  484. list.remove(path);
  485. }
  486. identityController.getUserSettings().setOption("plugins", "autoload", list);
  487. }
  488. /**
  489. * Get Collection&lt;PluginInf&gt; of known plugins.
  490. *
  491. * @return Collection&lt;PluginInfo&gt; of known plugins.
  492. */
  493. public Collection<PluginInfo> getPluginInfos() {
  494. return new ArrayList<>(knownPlugins.values());
  495. }
  496. @Handler
  497. public void handlePrefsOpened(final ClientPrefsOpenedEvent event) {
  498. for (PluginInfo pi : getPluginInfos()) {
  499. if (!pi.isLoaded() && !pi.isTempLoaded()) {
  500. pi.loadPluginTemp();
  501. }
  502. if (pi.isLoaded() || pi.isTempLoaded()) {
  503. try {
  504. pi.getPlugin().showConfig(event.getModel());
  505. } catch (LinkageError | Exception le) {
  506. eventBus.publishAsync(new AppErrorEvent(ErrorLevel.MEDIUM, le,
  507. "Unable to show plugin configuration for "
  508. + pi.getMetaData().getFriendlyName(), ""));
  509. }
  510. }
  511. }
  512. }
  513. @Handler
  514. public void handlePrefsClosed(final ClientPrefsClosedEvent event) {
  515. for (PluginInfo pi : getPluginInfos()) {
  516. if (pi.isTempLoaded()) {
  517. pi.unloadPlugin();
  518. }
  519. }
  520. }
  521. }