Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

PluginManager.java 21KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601
  1. /*
  2. * Copyright (c) 2006-2013 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.actions.CoreActionType;
  24. import com.dmdirc.config.prefs.PreferencesDialogModel;
  25. import com.dmdirc.interfaces.ActionController;
  26. import com.dmdirc.interfaces.ActionListener;
  27. import com.dmdirc.interfaces.actions.ActionType;
  28. import com.dmdirc.interfaces.config.IdentityController;
  29. import com.dmdirc.logger.ErrorLevel;
  30. import com.dmdirc.logger.Logger;
  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. /**
  49. * Searches for and manages plugins and services.
  50. */
  51. public class PluginManager implements ActionListener, ServiceManager {
  52. /** List of known plugins' file names to their corresponding {@link PluginInfo} objects. */
  53. private final Map<String, PluginInfo> knownPlugins = new HashMap<>();
  54. /** Set of known plugins' metadata. */
  55. private final Collection<PluginMetaData> plugins = new HashSet<>();
  56. /** Directory where plugins are stored. */
  57. private final String directory;
  58. /** The identity controller to use to find configuration options. */
  59. private final IdentityController identityController;
  60. /** The action controller to use for events. */
  61. private final ActionController actionController;
  62. /** The update manager to inform about plugins. */
  63. private final UpdateManager updateManager;
  64. /** A provider of initialisers for plugin injectors. */
  65. private final Provider<PluginInjectorInitialiser> initialiserProvider;
  66. /** Map of services. */
  67. private final Map<String, Map<String, Service>> services = new HashMap<>();
  68. /** Global ClassLoader used by plugins from this manager. */
  69. private final GlobalClassLoader globalClassLoader;
  70. /** The graph to pass to plugins for DI purposes. */
  71. private final ObjectGraph objectGraph;
  72. /**
  73. * Creates a new instance of PluginManager.
  74. *
  75. * @param identityController The identity controller to use for configuration options.
  76. * @param actionController The action controller to use for events.
  77. * @param updateManager The update manager to inform about plugins.
  78. * @param initialiserProvider A provider of initialisers for plugin injectors.
  79. * @param objectGraph The graph to pass to plugins for DI purposes.
  80. * @param directory The directory to load plugins from.
  81. */
  82. public PluginManager(
  83. final IdentityController identityController,
  84. final ActionController actionController,
  85. final UpdateManager updateManager,
  86. final Provider<PluginInjectorInitialiser> initialiserProvider,
  87. final ObjectGraph objectGraph,
  88. final String directory) {
  89. this.identityController = identityController;
  90. this.actionController = actionController;
  91. this.updateManager = updateManager;
  92. this.initialiserProvider = initialiserProvider;
  93. this.directory = directory;
  94. this.globalClassLoader = new GlobalClassLoader(this);
  95. this.objectGraph = objectGraph;
  96. actionController.registerListener(this,
  97. CoreActionType.CLIENT_PREFS_OPENED,
  98. CoreActionType.CLIENT_PREFS_CLOSED);
  99. }
  100. /**
  101. * Get the global class loader in use for this plugin manager.
  102. *
  103. * @return Global Class Loader
  104. */
  105. public GlobalClassLoader getGlobalClassLoader() {
  106. return globalClassLoader;
  107. }
  108. /** {@inheritDoc} */
  109. @Override
  110. public Service getService(final String type, final String name) {
  111. return getService(type, name, false);
  112. }
  113. /** {@inheritDoc} */
  114. @Override
  115. public Service getService(final String type, final String name, final boolean create) {
  116. // Find the type first
  117. if (services.containsKey(type)) {
  118. final Map<String, Service> map = services.get(type);
  119. // Now the name
  120. if (map.containsKey(name)) {
  121. return map.get(name);
  122. } else if (create) {
  123. final Service service = new Service(type, name);
  124. map.put(name, service);
  125. return service;
  126. }
  127. } else if (create) {
  128. final Map<String, Service> map = new HashMap<>();
  129. final Service service = new Service(type, name);
  130. map.put(name, service);
  131. services.put(type, map);
  132. return service;
  133. }
  134. return null;
  135. }
  136. /** {@inheritDoc} */
  137. @Override
  138. public ServiceProvider getServiceProvider(final String type, final String name) throws 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, final boolean fallback) throws NoSuchProviderException {
  158. for (final String name : names) {
  159. final ServiceProvider provider = getServiceProvider(type, name);
  160. if (provider != null) {
  161. return provider;
  162. }
  163. }
  164. if (fallback) {
  165. final List<Service> servicesType = getServicesByType(type);
  166. if (!servicesType.isEmpty()) {
  167. final Service service = servicesType.get(0);
  168. return getServiceProvider(type, service.getName());
  169. }
  170. }
  171. throw new NoSuchProviderException("No provider found for " + type + "from the given list");
  172. }
  173. /** {@inheritDoc} */
  174. @Override
  175. public ExportedService getExportedService(final String name) {
  176. return getServiceProvider("export", name).getExportedService(name);
  177. }
  178. /** {@inheritDoc} */
  179. @Override
  180. public List<Service> getServicesByType(final String type) {
  181. // Find the type first
  182. if (services.containsKey(type)) {
  183. final Map<String, Service> map = services.get(type);
  184. return new ArrayList<>(map.values());
  185. }
  186. return new ArrayList<>();
  187. }
  188. /** {@inheritDoc} */
  189. @Override
  190. public List<Service> getAllServices() {
  191. // Find the type first
  192. final List<Service> allServices = new ArrayList<>();
  193. for (Map<String, Service> map : services.values()) {
  194. allServices.addAll(map.values());
  195. }
  196. return allServices;
  197. }
  198. /**
  199. * Autoloads plugins.
  200. */
  201. public void doAutoLoad() {
  202. for (String plugin : identityController.getGlobalConfiguration().getOptionList("plugins", "autoload")) {
  203. plugin = plugin.trim();
  204. if (!plugin.isEmpty() && plugin.charAt(0) != '#' && getPluginInfo(plugin) != null) {
  205. getPluginInfo(plugin).loadPlugin();
  206. }
  207. }
  208. }
  209. /**
  210. * Tests and adds the specified plugin to the known plugins list. Plugins
  211. * will only be added if: <ul><li>The file exists,<li>No other plugin with
  212. * the same name is known,<li>All requirements are met for the plugin,
  213. * <li>The plugin has a valid config file that can be read</ul>.
  214. *
  215. * @param filename Filename of Plugin jar
  216. * @return True if the plugin is in the known plugins list (either before
  217. * this invocation or as a result of it), false if it was not added for
  218. * one of the reasons outlined above.
  219. */
  220. public boolean addPlugin(final String filename) {
  221. if (knownPlugins.containsKey(filename.toLowerCase())) {
  222. return true;
  223. }
  224. if (!new File(getDirectory() + filename).exists()) {
  225. Logger.userError(ErrorLevel.MEDIUM, "Error loading plugin "
  226. + filename + ": File does not exist");
  227. return false;
  228. }
  229. try {
  230. final PluginMetaData metadata = new PluginMetaData(this,
  231. new URL("jar:file:" + getDirectory() + filename
  232. + "!/META-INF/plugin.config"),
  233. new URL("file:" + getDirectory() + filename));
  234. metadata.load();
  235. final PluginInfo pluginInfo = new PluginInfo(metadata, initialiserProvider, objectGraph);
  236. final PluginInfo existing = getPluginInfoByName(metadata.getName());
  237. if (existing != null) {
  238. Logger.userError(ErrorLevel.MEDIUM,
  239. "Duplicate Plugin detected, Ignoring. (" + filename
  240. + " is the same as " + existing.getFilename() + ")");
  241. return false;
  242. }
  243. if ((metadata.getUpdaterId() > 0 && metadata.getVersion().isValid())
  244. || (identityController.getGlobalConfiguration()
  245. .hasOptionInt("plugin-addonid", metadata.getName()))) {
  246. updateManager.addComponent(new PluginComponent(pluginInfo));
  247. }
  248. knownPlugins.put(filename.toLowerCase(), pluginInfo);
  249. actionController.triggerEvent(CoreActionType.PLUGIN_REFRESH, null, this);
  250. return true;
  251. } catch (MalformedURLException mue) {
  252. Logger.userError(ErrorLevel.MEDIUM, "Error creating URL for plugin "
  253. + filename + ": " + mue.getMessage(), mue);
  254. } catch (PluginException e) {
  255. Logger.userError(ErrorLevel.MEDIUM, "Error loading plugin "
  256. + filename + ": " + e.getMessage(), e);
  257. }
  258. return false;
  259. }
  260. /**
  261. * Remove a plugin.
  262. *
  263. * @param filename Filename of Plugin jar
  264. * @return True if removed.
  265. */
  266. public boolean delPlugin(final String filename) {
  267. if (!knownPlugins.containsKey(filename.toLowerCase())) {
  268. return false;
  269. }
  270. final PluginInfo pluginInfo = getPluginInfo(filename);
  271. final boolean wasLoaded = pluginInfo.isLoaded();
  272. if (wasLoaded && !pluginInfo.isUnloadable()) { return false; }
  273. pluginInfo.unloadPlugin();
  274. knownPlugins.remove(filename.toLowerCase());
  275. return true;
  276. }
  277. /**
  278. * Reload a plugin.
  279. *
  280. * @param filename Filename of Plugin jar
  281. * @return True if reloaded.
  282. */
  283. public boolean reloadPlugin(final String filename) {
  284. if (!knownPlugins.containsKey(filename.toLowerCase())) {
  285. return false;
  286. }
  287. final PluginInfo pluginInfo = getPluginInfo(filename);
  288. final boolean wasLoaded = pluginInfo.isLoaded();
  289. if (wasLoaded && !pluginInfo.isUnloadable()) { return false; }
  290. delPlugin(filename);
  291. boolean result = addPlugin(filename);
  292. if (wasLoaded && result) {
  293. getPluginInfo(filename).loadPlugin();
  294. result = getPluginInfo(filename).isLoaded();
  295. }
  296. return result;
  297. }
  298. /**
  299. * Reload all plugins.
  300. */
  301. public void reloadAllPlugins() {
  302. for (PluginInfo pluginInfo : getPluginInfos()) {
  303. reloadPlugin(pluginInfo.getFilename());
  304. }
  305. }
  306. /**
  307. * Get a plugin instance.
  308. *
  309. * @param filename File name of plugin jar
  310. * @return PluginInfo instance, or null
  311. */
  312. public PluginInfo getPluginInfo(final String filename) {
  313. return knownPlugins.get(filename.toLowerCase());
  314. }
  315. /**
  316. * Get a plugin instance by plugin name.
  317. *
  318. * @param name Name of plugin to find.
  319. * @return PluginInfo instance, or null
  320. */
  321. public PluginInfo getPluginInfoByName(final String name) {
  322. for (PluginInfo pluginInfo : getPluginInfos()) {
  323. if (pluginInfo.getMetaData().getName().equalsIgnoreCase(name)) {
  324. return pluginInfo;
  325. }
  326. }
  327. return null;
  328. }
  329. /**
  330. * Get directory where plugins are stored.
  331. *
  332. * @return Directory where plugins are stored.
  333. */
  334. public String getDirectory() {
  335. return directory;
  336. }
  337. /**
  338. * Get directory where plugin files are stored.
  339. *
  340. * @return Directory where plugin files are stored.
  341. */
  342. public String getFilesDirectory() {
  343. final String fs = System.getProperty("file.separator");
  344. String filesDir = directory + "files" + fs;
  345. if (identityController.getGlobalConfiguration().hasOptionString("plugins", "filesdir")) {
  346. final String fdopt = identityController.getGlobalConfiguration()
  347. .getOptionString("plugins", "filesdir");
  348. if (fdopt != null && !fdopt.isEmpty() && new File(fdopt).exists()) {
  349. filesDir = fdopt;
  350. }
  351. }
  352. return filesDir;
  353. }
  354. /**
  355. * Refreshes the list of known plugins.
  356. */
  357. public void refreshPlugins() {
  358. applyUpdates();
  359. final Collection<PluginMetaData> newPlugins = getAllPlugins();
  360. for (PluginMetaData plugin : newPlugins) {
  361. addPlugin(plugin.getRelativeFilename());
  362. }
  363. // Update our list of plugins
  364. synchronized (plugins) {
  365. plugins.removeAll(newPlugins);
  366. for (PluginMetaData oldPlugin : new HashSet<>(plugins)) {
  367. delPlugin(oldPlugin.getRelativeFilename());
  368. }
  369. plugins.clear();
  370. plugins.addAll(newPlugins);
  371. }
  372. actionController.triggerEvent(CoreActionType.PLUGIN_REFRESH, null, this);
  373. }
  374. /**
  375. * Recursively scans the plugin directory and attempts to apply any
  376. * available updates.
  377. */
  378. public void applyUpdates() {
  379. final Deque<File> dirs = new LinkedList<>();
  380. dirs.add(new File(directory));
  381. while (!dirs.isEmpty()) {
  382. final File dir = dirs.pop();
  383. if (dir.isDirectory()) {
  384. dirs.addAll(Arrays.asList(dir.listFiles()));
  385. } else if (dir.isFile() && dir.getName().endsWith(".jar")) {
  386. final File update = new File(dir.getAbsolutePath() + ".update");
  387. if (update.exists() && dir.delete()) {
  388. update.renameTo(dir);
  389. }
  390. }
  391. }
  392. }
  393. /**
  394. * Retrieves a list of all installed plugins.
  395. * Any file under the main plugin directory (~/.DMDirc/plugins or similar)
  396. * that matches *.jar is deemed to be a valid plugin.
  397. *
  398. * @return A list of all installed or known plugins
  399. */
  400. public Collection<PluginMetaData> getAllPlugins() {
  401. final Collection<PluginMetaData> res = new HashSet<>(plugins.size());
  402. final Deque<File> dirs = new LinkedList<>();
  403. final Collection<String> pluginPaths = new LinkedList<>();
  404. dirs.add(new File(directory));
  405. while (!dirs.isEmpty()) {
  406. final File dir = dirs.pop();
  407. if (dir.isDirectory()) {
  408. dirs.addAll(Arrays.asList(dir.listFiles()));
  409. } else if (dir.isFile() && dir.getName().endsWith(".jar")) {
  410. pluginPaths.add(dir.getPath().substring(directory.length()));
  411. }
  412. }
  413. final MapList<String, String> newServices = new MapList<>();
  414. final Map<String, PluginMetaData> newPluginsByName = new HashMap<>();
  415. final Map<String, PluginMetaData> newPluginsByPath = new HashMap<>();
  416. // Initialise all of our metadata objects
  417. for (String target : pluginPaths) {
  418. try {
  419. final PluginMetaData targetMetaData = new PluginMetaData(this,
  420. new URL("jar:file:" + getDirectory() + target
  421. + "!/META-INF/plugin.config"),
  422. new URL("file:" + getDirectory() + target));
  423. targetMetaData.load();
  424. if (targetMetaData.hasErrors()) {
  425. Logger.userError(ErrorLevel.MEDIUM,
  426. "Error reading plugin metadata for plugin " + target
  427. + ": " + targetMetaData.getErrors());
  428. } else {
  429. newPluginsByName.put(targetMetaData.getName(), targetMetaData);
  430. newPluginsByPath.put(target, targetMetaData);
  431. for (String service : targetMetaData.getServices()) {
  432. final String[] parts = service.split(" ", 2);
  433. newServices.add(parts[1], parts[0]);
  434. }
  435. for (String export : targetMetaData.getExports()) {
  436. final String[] parts = export.split(" ");
  437. final String name = parts.length > 4 ? parts[4] : parts[0];
  438. newServices.add("export", name);
  439. }
  440. }
  441. } catch (MalformedURLException mue) {
  442. Logger.userError(ErrorLevel.MEDIUM,
  443. "Error creating URL for plugin " + target + ": "
  444. + mue.getMessage(), mue);
  445. }
  446. }
  447. // Now validate all of the plugins
  448. for (Map.Entry<String, PluginMetaData> target : newPluginsByPath.entrySet()) {
  449. final PluginMetaDataValidator validator
  450. = new PluginMetaDataValidator(target.getValue());
  451. final Collection<String> results
  452. = validator.validate(newPluginsByName, newServices);
  453. if (results.isEmpty()) {
  454. res.add(target.getValue());
  455. } else {
  456. Logger.userError(ErrorLevel.MEDIUM, "Plugin validation failed for "
  457. + target.getKey() + ": " + results);
  458. }
  459. }
  460. return res;
  461. }
  462. /**
  463. * Update the autoLoadList
  464. *
  465. * @param plugin to add/remove (Decided automatically based on isLoaded())
  466. */
  467. public void updateAutoLoad(final PluginInfo plugin) {
  468. final List<String> list = identityController.getGlobalConfiguration()
  469. .getOptionList("plugins", "autoload");
  470. final String path = plugin.getMetaData().getRelativeFilename();
  471. if (plugin.isLoaded() && !list.contains(path)) {
  472. list.add(path);
  473. } else if (!plugin.isLoaded() && list.contains(path)) {
  474. list.remove(path);
  475. }
  476. identityController.getUserSettings().setOption("plugins", "autoload", list);
  477. }
  478. /**
  479. * Get Collection&lt;PluginInf&gt; of known plugins.
  480. *
  481. * @return Collection&lt;PluginInfo&gt; of known plugins.
  482. */
  483. public Collection<PluginInfo> getPluginInfos() {
  484. return new ArrayList<>(knownPlugins.values());
  485. }
  486. /** {@inheritDoc} */
  487. @Override
  488. public void processEvent(final ActionType type, final StringBuffer format, final Object... arguments) {
  489. if (type.equals(CoreActionType.CLIENT_PREFS_OPENED)) {
  490. for (PluginInfo pi : getPluginInfos()) {
  491. if (!pi.isLoaded() && !pi.isTempLoaded()) {
  492. pi.loadPluginTemp();
  493. }
  494. if (pi.isLoaded() || pi.isTempLoaded()) {
  495. try {
  496. pi.getPlugin().showConfig((PreferencesDialogModel) arguments[0]);
  497. } catch (LinkageError | Exception le) {
  498. Logger.userError(ErrorLevel.MEDIUM,
  499. "Error with plugin (" + pi.getMetaData().getFriendlyName()
  500. + "), unable to show config (" + le + ")", le);
  501. }
  502. }
  503. }
  504. } else if (type.equals(CoreActionType.CLIENT_PREFS_CLOSED)) {
  505. for (PluginInfo pi : getPluginInfos()) {
  506. if (pi.isTempLoaded()) {
  507. pi.unloadPlugin();
  508. }
  509. }
  510. }
  511. }
  512. }