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.

ServerPluginRepository.java 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. /*
  2. * SonarQube, open source software quality management tool.
  3. * Copyright (C) 2008-2014 SonarSource
  4. * mailto:contact AT sonarsource DOT com
  5. *
  6. * SonarQube is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * SonarQube is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. package org.sonar.server.plugins;
  21. import com.google.common.annotations.VisibleForTesting;
  22. import com.google.common.base.Function;
  23. import com.google.common.base.Joiner;
  24. import com.google.common.base.Strings;
  25. import com.google.common.collect.ImmutableSet;
  26. import com.google.common.collect.Ordering;
  27. import org.apache.commons.io.FileUtils;
  28. import org.picocontainer.Startable;
  29. import org.sonar.api.Plugin;
  30. import org.sonar.api.platform.Server;
  31. import org.sonar.api.platform.ServerUpgradeStatus;
  32. import org.sonar.api.utils.MessageException;
  33. import org.sonar.api.utils.log.Logger;
  34. import org.sonar.api.utils.log.Loggers;
  35. import org.sonar.core.platform.PluginInfo;
  36. import org.sonar.core.platform.PluginLoader;
  37. import org.sonar.core.platform.PluginRepository;
  38. import org.sonar.server.platform.DefaultServerFileSystem;
  39. import javax.annotation.Nonnull;
  40. import java.io.File;
  41. import java.io.IOException;
  42. import java.util.ArrayList;
  43. import java.util.Collection;
  44. import java.util.Collections;
  45. import java.util.HashMap;
  46. import java.util.List;
  47. import java.util.Map;
  48. import java.util.Set;
  49. import static com.google.common.collect.Iterables.transform;
  50. import static com.google.common.collect.Lists.newArrayList;
  51. import static java.lang.String.format;
  52. import static org.apache.commons.io.FileUtils.copyFile;
  53. import static org.apache.commons.io.FileUtils.deleteQuietly;
  54. import static org.apache.commons.io.FileUtils.moveFile;
  55. import static org.apache.commons.io.FileUtils.moveFileToDirectory;
  56. /**
  57. * Manages installation and loading of plugins:
  58. * <ul>
  59. * <li>installation of bundled plugins on first server startup</li>
  60. * <li>installation of new plugins (effective after server startup)</li>
  61. * <li>un-installation of plugins (effective after server startup)</li>
  62. * <li>cancel pending installations/un-installations</li>
  63. * <li>load plugin bytecode</li>
  64. * </ul>
  65. */
  66. public class ServerPluginRepository implements PluginRepository, Startable {
  67. private static final Logger LOG = Loggers.get(ServerPluginRepository.class);
  68. private static final String FILE_EXTENSION_JAR = "jar";
  69. private static final Set<String> DEFAULT_BLACKLISTED_PLUGINS = ImmutableSet.of("scmactivity", "issuesreport");
  70. private static final Joiner SLASH_JOINER = Joiner.on(" / ").skipNulls();
  71. private final Server server;
  72. private final DefaultServerFileSystem fs;
  73. private final ServerUpgradeStatus upgradeStatus;
  74. private final PluginLoader loader;
  75. private Set<String> blacklistedPluginKeys = DEFAULT_BLACKLISTED_PLUGINS;
  76. // following fields are available after startup
  77. private final Map<String, PluginInfo> pluginInfosByKeys = new HashMap<>();
  78. private final Map<String, Plugin> pluginInstancesByKeys = new HashMap<>();
  79. public ServerPluginRepository(Server server, ServerUpgradeStatus upgradeStatus,
  80. DefaultServerFileSystem fs, PluginLoader loader) {
  81. this.server = server;
  82. this.upgradeStatus = upgradeStatus;
  83. this.fs = fs;
  84. this.loader = loader;
  85. }
  86. @VisibleForTesting
  87. void setBlacklistedPluginKeys(Set<String> keys) {
  88. this.blacklistedPluginKeys = keys;
  89. }
  90. @Override
  91. public void start() {
  92. loadPreInstalledPlugins();
  93. copyBundledPlugins();
  94. moveDownloadedPlugins();
  95. loadCorePlugins();
  96. unloadIncompatiblePlugins();
  97. logInstalledPlugins();
  98. loadInstances();
  99. }
  100. @Override
  101. public void stop() {
  102. // close classloaders
  103. loader.unload(pluginInstancesByKeys.values());
  104. pluginInstancesByKeys.clear();
  105. pluginInfosByKeys.clear();
  106. }
  107. /**
  108. * Load the plugins that are located in extensions/plugins. Blacklisted plugins are
  109. * deleted.
  110. */
  111. private void loadPreInstalledPlugins() {
  112. for (File file : listJarFiles(fs.getInstalledPluginsDir())) {
  113. PluginInfo info = PluginInfo.create(file);
  114. registerPluginInfo(info);
  115. }
  116. }
  117. /**
  118. * Move the plugins recently downloaded to extensions/plugins.
  119. */
  120. private void moveDownloadedPlugins() {
  121. if (fs.getDownloadedPluginsDir().exists()) {
  122. for (File sourceFile : listJarFiles(fs.getDownloadedPluginsDir())) {
  123. overrideAndRegisterPlugin(sourceFile, true);
  124. }
  125. }
  126. }
  127. /**
  128. * Copies the plugins bundled with SonarQube distribution to directory extensions/plugins.
  129. * Does nothing if not a fresh installation.
  130. */
  131. private void copyBundledPlugins() {
  132. if (upgradeStatus.isFreshInstall()) {
  133. for (File sourceFile : listJarFiles(fs.getBundledPluginsDir())) {
  134. PluginInfo info = PluginInfo.create(sourceFile);
  135. // lib/bundled-plugins should be copied only if the plugin is not already
  136. // available in extensions/plugins
  137. if (!pluginInfosByKeys.containsKey(info.getKey())) {
  138. overrideAndRegisterPlugin(sourceFile, false);
  139. }
  140. }
  141. }
  142. }
  143. private void registerPluginInfo(PluginInfo info) {
  144. if (blacklistedPluginKeys.contains(info.getKey())) {
  145. LOG.warn("Plugin {} [{}] is blacklisted and is being uninstalled.", info.getName(), info.getKey());
  146. deleteQuietly(info.getFile());
  147. return;
  148. }
  149. PluginInfo existing = pluginInfosByKeys.put(info.getKey(), info);
  150. if (existing != null) {
  151. throw MessageException.of(format("Found two files for the same plugin [%s]: %s and %s",
  152. info.getKey(), info.getFile().getName(), existing.getFile().getName()));
  153. }
  154. }
  155. /**
  156. * Move or copy plugin to directory extensions/plugins. If a version of this plugin
  157. * already exists then it's deleted.
  158. */
  159. private void overrideAndRegisterPlugin(File sourceFile, boolean deleteSource) {
  160. File destDir = fs.getInstalledPluginsDir();
  161. File destFile = new File(destDir, sourceFile.getName());
  162. if (destFile.exists()) {
  163. // plugin with same filename already installed
  164. deleteQuietly(destFile);
  165. }
  166. try {
  167. if (deleteSource) {
  168. moveFile(sourceFile, destFile);
  169. } else {
  170. copyFile(sourceFile, destFile, true);
  171. }
  172. } catch (IOException e) {
  173. LOG.error(format("Fail to move or copy plugin: %s to %s",
  174. sourceFile.getAbsolutePath(), destFile.getAbsolutePath()), e);
  175. }
  176. PluginInfo info = PluginInfo.create(destFile);
  177. PluginInfo existing = pluginInfosByKeys.put(info.getKey(), info);
  178. if (existing != null) {
  179. if (!existing.getFile().getName().equals(destFile.getName())) {
  180. deleteQuietly(existing.getFile());
  181. }
  182. LOG.info("Plugin {} [{}] updated to version {}", info.getName(), info.getKey(), info.getVersion());
  183. } else {
  184. LOG.info("Plugin {} [{}] installed", info.getName(), info.getKey());
  185. }
  186. }
  187. private void loadCorePlugins() {
  188. for (File file : listJarFiles(fs.getCorePluginsDir())) {
  189. PluginInfo info = PluginInfo.create(file).setCore(true);
  190. registerPluginInfo(info);
  191. }
  192. }
  193. /**
  194. * Removes the plugins that are not compatible with current environment. In some cases
  195. * plugin files can be deleted.
  196. */
  197. private void unloadIncompatiblePlugins() {
  198. // loop as long as the previous loop ignored some plugins. That allows to support dependencies
  199. // on many levels, for example D extends C, which extends B, which requires A. If A is not installed,
  200. // then B, C and D must be ignored. That's not possible to achieve this algorithm with a single
  201. // iteration over plugins.
  202. List<String> removedKeys = new ArrayList<>();
  203. do {
  204. removedKeys.clear();
  205. for (PluginInfo plugin : pluginInfosByKeys.values()) {
  206. if (!Strings.isNullOrEmpty(plugin.getBasePlugin()) && !pluginInfosByKeys.containsKey(plugin.getBasePlugin())) {
  207. // this plugin extends a plugin that is not installed
  208. LOG.warn("Plugin {} [{}] is ignored because its base plugin [{}] is not installed", plugin.getName(), plugin.getKey(), plugin.getBasePlugin());
  209. removedKeys.add(plugin.getKey());
  210. }
  211. if (!plugin.isCompatibleWith(server.getVersion())) {
  212. LOG.warn("Plugin {} [{}] is ignored because it requires at least SonarQube {}", plugin.getName(), plugin.getKey(), plugin.getMinimalSqVersion());
  213. removedKeys.add(plugin.getKey());
  214. }
  215. for (PluginInfo.RequiredPlugin requiredPlugin : plugin.getRequiredPlugins()) {
  216. PluginInfo available = pluginInfosByKeys.get(requiredPlugin.getKey());
  217. if (available == null) {
  218. // this plugin requires a plugin that is not installed
  219. LOG.warn("Plugin {} [{}] is ignored because the required plugin [{}] is not installed", plugin.getName(), plugin.getKey(), requiredPlugin.getKey());
  220. removedKeys.add(plugin.getKey());
  221. } else if (requiredPlugin.getMinimalVersion().compareToIgnoreQualifier(available.getVersion()) > 0) {
  222. LOG.warn("Plugin {} [{}] is ignored because the version {} of required plugin [{}] is not supported", plugin.getName(), plugin.getKey(),
  223. requiredPlugin.getKey(), requiredPlugin.getMinimalVersion());
  224. removedKeys.add(plugin.getKey());
  225. }
  226. }
  227. }
  228. for (String removedKey : removedKeys) {
  229. pluginInfosByKeys.remove(removedKey);
  230. }
  231. } while (!removedKeys.isEmpty());
  232. }
  233. private void logInstalledPlugins() {
  234. List<PluginInfo> orderedPlugins = Ordering.natural().sortedCopy(pluginInfosByKeys.values());
  235. for (PluginInfo plugin : orderedPlugins) {
  236. LOG.info("Plugin: {}", SLASH_JOINER.join(plugin.getName(), plugin.getVersion(), plugin.getImplementationBuild()));
  237. }
  238. }
  239. private void loadInstances() {
  240. pluginInstancesByKeys.clear();
  241. pluginInstancesByKeys.putAll(loader.load(pluginInfosByKeys));
  242. }
  243. /**
  244. * Uninstall a plugin and its dependents (the plugins that require the plugin to be uninstalled)
  245. */
  246. public void uninstall(String pluginKey) {
  247. for (PluginInfo otherPlugin : pluginInfosByKeys.values()) {
  248. if (!otherPlugin.getKey().equals(pluginKey)) {
  249. for (PluginInfo.RequiredPlugin requiredPlugin : otherPlugin.getRequiredPlugins()) {
  250. if (requiredPlugin.getKey().equals(pluginKey)) {
  251. uninstall(otherPlugin.getKey());
  252. }
  253. }
  254. }
  255. }
  256. PluginInfo info = pluginInfosByKeys.get(pluginKey);
  257. if (!info.isCore()) {
  258. try {
  259. // we don't reuse info.getFile() just to be sure that file is located in from extensions/plugins
  260. File masterFile = new File(fs.getInstalledPluginsDir(), info.getFile().getName());
  261. moveFileToDirectory(masterFile, uninstalledPluginsDir(), true);
  262. } catch (IOException e) {
  263. throw new IllegalStateException("Fail to uninstall plugin [" + pluginKey + "]", e);
  264. }
  265. }
  266. }
  267. public List<String> getUninstalledPluginFilenames() {
  268. return newArrayList(transform(listJarFiles(uninstalledPluginsDir()), FileToName.INSTANCE));
  269. }
  270. /**
  271. * @return the list of plugins to be uninstalled as {@link PluginInfo} instances
  272. */
  273. public Collection<PluginInfo> getUninstalledPlugins() {
  274. return newArrayList(transform(listJarFiles(uninstalledPluginsDir()), PluginInfo.JarToPluginInfo.INSTANCE));
  275. }
  276. public void cancelUninstalls() {
  277. for (File file : listJarFiles(uninstalledPluginsDir())) {
  278. try {
  279. moveFileToDirectory(file, fs.getInstalledPluginsDir(), false);
  280. } catch (IOException e) {
  281. throw new IllegalStateException("Fail to cancel plugin uninstalls", e);
  282. }
  283. }
  284. }
  285. public Map<String, PluginInfo> getPluginInfosByKeys() {
  286. return pluginInfosByKeys;
  287. }
  288. @Override
  289. public Collection<PluginInfo> getPluginInfos() {
  290. return pluginInfosByKeys.values();
  291. }
  292. @Override
  293. public PluginInfo getPluginInfo(String key) {
  294. PluginInfo info = pluginInfosByKeys.get(key);
  295. if (info == null) {
  296. throw new IllegalArgumentException(String.format("Plugin [%s] does not exist", key));
  297. }
  298. return info;
  299. }
  300. @Override
  301. public Plugin getPluginInstance(String key) {
  302. Plugin plugin = pluginInstancesByKeys.get(key);
  303. if (plugin == null) {
  304. throw new IllegalArgumentException(String.format("Plugin [%s] does not exist", key));
  305. }
  306. return plugin;
  307. }
  308. @Override
  309. public boolean hasPlugin(String key) {
  310. return pluginInfosByKeys.containsKey(key);
  311. }
  312. private enum FileToName implements Function<File, String> {
  313. INSTANCE;
  314. @Override
  315. public String apply(@Nonnull File file) {
  316. return file.getName();
  317. }
  318. }
  319. /**
  320. * @return existing trash dir
  321. */
  322. private File uninstalledPluginsDir() {
  323. File dir = new File(fs.getTempDir(), "uninstalled-plugins");
  324. try {
  325. FileUtils.forceMkdir(dir);
  326. return dir;
  327. } catch (IOException e) {
  328. throw new IllegalStateException("Fail to create temp directory: " + dir.getAbsolutePath(), e);
  329. }
  330. }
  331. private static Collection<File> listJarFiles(File dir) {
  332. if (dir.exists()) {
  333. return FileUtils.listFiles(dir, new String[] {FILE_EXTENSION_JAR}, false);
  334. }
  335. return Collections.emptyList();
  336. }
  337. }