/hudson-core/src/main/java/hudson/PluginWrapper.java

http://github.com/hudson/hudson · Java · 560 lines · 276 code · 72 blank · 212 comment · 27 complexity · 5490aa76f743686341b01210143654ac MD5 · raw file

  1. /*
  2. * The MIT License
  3. *
  4. * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
  5. * Yahoo! Inc., Erik Ramfelt, Tom Huybrechts
  6. *
  7. * Permission is hereby granted, free of charge, to any person obtaining a copy
  8. * of this software and associated documentation files (the "Software"), to deal
  9. * in the Software without restriction, including without limitation the rights
  10. * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11. * copies of the Software, and to permit persons to whom the Software is
  12. * furnished to do so, subject to the following conditions:
  13. *
  14. * The above copyright notice and this permission notice shall be included in
  15. * all copies or substantial portions of the Software.
  16. *
  17. * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18. * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19. * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20. * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21. * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22. * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23. * THE SOFTWARE.
  24. */
  25. package hudson;
  26. import hudson.PluginManager.PluginInstanceStore;
  27. import hudson.model.Hudson;
  28. import hudson.model.UpdateCenter;
  29. import hudson.model.UpdateSite;
  30. import hudson.util.VersionNumber;
  31. import java.io.File;
  32. import java.io.FileOutputStream;
  33. import java.io.IOException;
  34. import java.io.OutputStream;
  35. import java.io.Closeable;
  36. import java.net.URL;
  37. import java.util.ArrayList;
  38. import java.util.List;
  39. import java.util.jar.Manifest;
  40. import java.util.logging.Logger;
  41. import static java.util.logging.Level.WARNING;
  42. import org.apache.commons.logging.LogFactory;
  43. import org.kohsuke.stapler.HttpResponse;
  44. import org.kohsuke.stapler.HttpResponses;
  45. import java.util.Enumeration;
  46. import java.util.jar.JarFile;
  47. /**
  48. * Represents a Hudson plug-in and associated control information
  49. * for Hudson to control {@link Plugin}.
  50. *
  51. * <p>
  52. * A plug-in is packaged into a jar file whose extension is <tt>".hpi"</tt>,
  53. * A plugin needs to have a special manifest entry to identify what it is.
  54. *
  55. * <p>
  56. * At the runtime, a plugin has two distinct state axis.
  57. * <ol>
  58. * <li>Enabled/Disabled. If enabled, Hudson is going to use it
  59. * next time Hudson runs. Otherwise the next run will ignore it.
  60. * <li>Activated/Deactivated. If activated, that means Hudson is using
  61. * the plugin in this session. Otherwise it's not.
  62. * </ol>
  63. * <p>
  64. * For example, an activated but disabled plugin is still running but the next
  65. * time it won't.
  66. *
  67. * @author Kohsuke Kawaguchi
  68. */
  69. public class PluginWrapper implements Comparable<PluginWrapper> {
  70. /**
  71. * {@link PluginManager} to which this belongs to.
  72. */
  73. //TODO: review and check whether we can do it private
  74. public final PluginManager parent;
  75. /**
  76. * Plugin manifest.
  77. * Contains description of the plugin.
  78. */
  79. private final Manifest manifest;
  80. /**
  81. * {@link ClassLoader} for loading classes from this plugin.
  82. * Null if disabled.
  83. */
  84. //TODO: review and check whether we can do it private
  85. public final ClassLoader classLoader;
  86. /**
  87. * Base URL for loading static resources from this plugin.
  88. * Null if disabled. The static resources are mapped under
  89. * <tt>hudson/plugin/SHORTNAME/</tt>.
  90. */
  91. //TODO: review and check whether we can do it private
  92. public final URL baseResourceURL;
  93. /**
  94. * Used to control enable/disable setting of the plugin.
  95. * If this file exists, plugin will be disabled.
  96. */
  97. private final File disableFile;
  98. /**
  99. * Used to control the unpacking of the bundled plugin.
  100. * If a pin file exists, Hudson assumes that the user wants to pin down a particular version
  101. * of a plugin, and will not try to overwrite it. Otherwise, it'll be overwritten
  102. * by a bundled copy, to ensure consistency across upgrade/downgrade.
  103. * @since 1.325
  104. */
  105. private final File pinFile;
  106. /**
  107. * Short name of the plugin. The artifact Id of the plugin.
  108. * This is also used in the URL within Hudson, so it needs
  109. * to remain stable even when the *.hpi file name is changed
  110. * (like Maven does.)
  111. */
  112. private final String shortName;
  113. /**
  114. * True if this plugin is activated for this session.
  115. * The snapshot of <tt>disableFile.exists()</tt> as of the start up.
  116. */
  117. private final boolean active;
  118. private final List<Dependency> dependencies;
  119. private final List<Dependency> optionalDependencies;
  120. /**
  121. * Is this plugin bundled in hudson.war?
  122. */
  123. /*package*/ boolean isBundled;
  124. public static final class Dependency {
  125. //TODO: review and check whether we can do it private
  126. public final String shortName;
  127. public final String version;
  128. public final boolean optional;
  129. public Dependency(String s) {
  130. int idx = s.indexOf(':');
  131. if(idx==-1)
  132. throw new IllegalArgumentException("Illegal dependency specifier "+s);
  133. this.shortName = s.substring(0,idx);
  134. this.version = s.substring(idx+1);
  135. boolean isOptional = false;
  136. String[] osgiProperties = s.split(";");
  137. for (int i = 1; i < osgiProperties.length; i++) {
  138. String osgiProperty = osgiProperties[i].trim();
  139. if (osgiProperty.equalsIgnoreCase("resolution:=optional")) {
  140. isOptional = true;
  141. }
  142. }
  143. this.optional = isOptional;
  144. }
  145. public String getShortName() {
  146. return shortName;
  147. }
  148. public String getVersion() {
  149. return version;
  150. }
  151. public boolean isOptional() {
  152. return optional;
  153. }
  154. @Override
  155. public String toString() {
  156. return shortName + " (" + version + ")";
  157. }
  158. }
  159. /**
  160. * @param archive
  161. * A .hpi archive file jar file, or a .hpl linked plugin.
  162. * @param manifest
  163. * The manifest for the plugin
  164. * @param baseResourceURL
  165. * A URL pointing to the resources for this plugin
  166. * @param classLoader
  167. * a classloader that loads classes from this plugin and its dependencies
  168. * @param disableFile
  169. * if this file exists on startup, the plugin will not be activated
  170. * @param dependencies a list of mandatory dependencies
  171. * @param optionalDependencies a list of optional dependencies
  172. */
  173. public PluginWrapper(PluginManager parent, File archive, Manifest manifest, URL baseResourceURL,
  174. ClassLoader classLoader, File disableFile,
  175. List<Dependency> dependencies, List<Dependency> optionalDependencies) {
  176. this.parent = parent;
  177. this.manifest = manifest;
  178. this.shortName = computeShortName(manifest, archive);
  179. this.baseResourceURL = baseResourceURL;
  180. this.classLoader = classLoader;
  181. this.disableFile = disableFile;
  182. this.pinFile = new File(archive.getPath() + ".pinned");
  183. this.active = !disableFile.exists();
  184. this.dependencies = dependencies;
  185. this.optionalDependencies = optionalDependencies;
  186. }
  187. public PluginManager getParent() {
  188. return parent;
  189. }
  190. public ClassLoader getClassLoader() {
  191. return classLoader;
  192. }
  193. public URL getBaseResourceURL() {
  194. return baseResourceURL;
  195. }
  196. /**
  197. * Returns the URL of the index page jelly script.
  198. */
  199. public URL getIndexPage() {
  200. // In the current impl dependencies are checked first, so the plugin itself
  201. // will add the last entry in the getResources result.
  202. URL idx = null;
  203. try {
  204. Enumeration<URL> en = classLoader.getResources("index.jelly");
  205. while (en.hasMoreElements())
  206. idx = en.nextElement();
  207. } catch (IOException ignore) { }
  208. // In case plugin has dependencies but is missing its own index.jelly,
  209. // check that result has this plugin's artifactId in it:
  210. return idx != null && idx.toString().contains(shortName) ? idx : null;
  211. }
  212. private String computeShortName(Manifest manifest, File archive) {
  213. // use the name captured in the manifest, as often plugins
  214. // depend on the specific short name in its URLs.
  215. String n = manifest.getMainAttributes().getValue("Short-Name");
  216. if(n!=null) return n;
  217. // maven seems to put this automatically, so good fallback to check.
  218. n = manifest.getMainAttributes().getValue("Extension-Name");
  219. if(n!=null) return n;
  220. // otherwise infer from the file name, since older plugins don't have
  221. // this entry.
  222. return getBaseName(archive);
  223. }
  224. /**
  225. * Gets the "abc" portion from "abc.ext".
  226. */
  227. static String getBaseName(File archive) {
  228. String n = archive.getName();
  229. int idx = n.lastIndexOf('.');
  230. if(idx>=0)
  231. n = n.substring(0,idx);
  232. return n;
  233. }
  234. public List<Dependency> getDependencies() {
  235. return dependencies;
  236. }
  237. public List<Dependency> getOptionalDependencies() {
  238. return optionalDependencies;
  239. }
  240. /**
  241. * Returns the short name suitable for URL.
  242. */
  243. public String getShortName() {
  244. return shortName;
  245. }
  246. /**
  247. * Gets the instance of {@link Plugin} contributed by this plugin.
  248. */
  249. public Plugin getPlugin() {
  250. return Hudson.lookup(PluginInstanceStore.class).store.get(this);
  251. }
  252. /**
  253. * Gets the URL that shows more information about this plugin.
  254. * @return
  255. * null if this information is unavailable.
  256. * @since 1.283
  257. */
  258. public String getUrl() {
  259. // first look for the manifest entry. This is new in maven-hpi-plugin 1.30
  260. String url = manifest.getMainAttributes().getValue("Url");
  261. if(url!=null) return url;
  262. // fallback to update center metadata
  263. UpdateSite.Plugin ui = getInfo();
  264. if(ui!=null) return ui.wiki;
  265. return null;
  266. }
  267. @Override
  268. public String toString() {
  269. return "Plugin:" + getShortName();
  270. }
  271. /**
  272. * Returns a one-line descriptive name of this plugin.
  273. */
  274. public String getLongName() {
  275. String name = manifest.getMainAttributes().getValue("Long-Name");
  276. if(name!=null) return name;
  277. return shortName;
  278. }
  279. /**
  280. * Returns the version number of this plugin
  281. */
  282. public String getVersion() {
  283. String v = manifest.getMainAttributes().getValue("Plugin-Version");
  284. if(v!=null) return v;
  285. // plugins generated before maven-hpi-plugin 1.3 should still have this attribute
  286. v = manifest.getMainAttributes().getValue("Implementation-Version");
  287. if(v!=null) return v;
  288. return "???";
  289. }
  290. /**
  291. * Returns the version number of this plugin
  292. */
  293. public VersionNumber getVersionNumber() {
  294. return new VersionNumber(getVersion());
  295. }
  296. /**
  297. * Returns true if the version of this plugin is older than the given version.
  298. */
  299. public boolean isOlderThan(VersionNumber v) {
  300. try {
  301. return getVersionNumber().compareTo(v) < 0;
  302. } catch (IllegalArgumentException e) {
  303. // if we can't figure out our current version, it probably means it's very old,
  304. // since the version information is missing only from the very old plugins
  305. return true;
  306. }
  307. }
  308. /**
  309. * Terminates the plugin.
  310. */
  311. public void stop() {
  312. LOGGER.info("Stopping "+shortName);
  313. try {
  314. getPlugin().stop();
  315. } catch(Throwable t) {
  316. LOGGER.log(WARNING, "Failed to shut down "+shortName, t);
  317. }
  318. // Work around a bug in commons-logging.
  319. // See http://www.szegedi.org/articles/memleak.html
  320. LogFactory.release(classLoader);
  321. }
  322. public void releaseClassLoader() {
  323. if (classLoader instanceof Closeable)
  324. try {
  325. ((Closeable) classLoader).close();
  326. } catch (IOException e) {
  327. LOGGER.log(WARNING, "Failed to shut down classloader",e);
  328. }
  329. }
  330. /**
  331. * Enables this plugin next time Hudson runs.
  332. */
  333. public void enable() throws IOException {
  334. if(!disableFile.delete())
  335. throw new IOException("Failed to delete "+disableFile);
  336. }
  337. /**
  338. * Disables this plugin next time Hudson runs.
  339. */
  340. public void disable() throws IOException {
  341. // creates an empty file
  342. OutputStream os = new FileOutputStream(disableFile);
  343. os.close();
  344. }
  345. /**
  346. * Returns true if this plugin is enabled for this session.
  347. */
  348. public boolean isActive() {
  349. return active;
  350. }
  351. public boolean isBundled() {
  352. return isBundled;
  353. }
  354. /**
  355. * If true, the plugin is going to be activated next time
  356. * Hudson runs.
  357. */
  358. public boolean isEnabled() {
  359. return !disableFile.exists();
  360. }
  361. public Manifest getManifest() {
  362. return manifest;
  363. }
  364. public void setPlugin(Plugin plugin) {
  365. Hudson.lookup(PluginInstanceStore.class).store.put(this,plugin);
  366. plugin.wrapper = this;
  367. }
  368. public String getPluginClass() {
  369. return manifest.getMainAttributes().getValue("Plugin-Class");
  370. }
  371. /**
  372. * Makes sure that all the dependencies exist, and then accept optional dependencies
  373. * as real dependencies.
  374. *
  375. * @throws IOException
  376. * thrown if one or several mandatory dependencies doesn't exists.
  377. */
  378. /*package*/ void resolvePluginDependencies() throws IOException {
  379. List<String> missingDependencies = new ArrayList<String>();
  380. // make sure dependencies exist
  381. for (Dependency d : dependencies) {
  382. if (parent.getPlugin(d.shortName) == null)
  383. missingDependencies.add(d.toString());
  384. }
  385. if (!missingDependencies.isEmpty())
  386. throw new IOException("Dependency "+Util.join(missingDependencies, ", ")+" doesn't exist");
  387. // add the optional dependencies that exists
  388. for (Dependency d : optionalDependencies) {
  389. if (parent.getPlugin(d.shortName) != null)
  390. dependencies.add(d);
  391. }
  392. }
  393. /**
  394. * If the plugin has {@link #getUpdateInfo() an update},
  395. * returns the {@link UpdateSite.Plugin} object.
  396. *
  397. * @return
  398. * This method may return null &mdash; for example,
  399. * the user may have installed a plugin locally developed.
  400. */
  401. public UpdateSite.Plugin getUpdateInfo() {
  402. UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
  403. UpdateSite.Plugin p = uc.getPlugin(getShortName());
  404. if(p!=null && p.isNewerThan(getVersion())) return p;
  405. return null;
  406. }
  407. /**
  408. * returns the {@link UpdateSite.Plugin} object, or null.
  409. */
  410. public UpdateSite.Plugin getInfo() {
  411. UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
  412. return uc.getPlugin(getShortName());
  413. }
  414. /**
  415. * Returns true if this plugin has update in the update center.
  416. *
  417. * <p>
  418. * This method is conservative in the sense that if the version number is incomprehensible,
  419. * it always returns false.
  420. */
  421. public boolean hasUpdate() {
  422. return getUpdateInfo()!=null;
  423. }
  424. public boolean isPinned() {
  425. return pinFile.exists();
  426. }
  427. /**
  428. * Sort by short name.
  429. */
  430. public int compareTo(PluginWrapper pw) {
  431. return shortName.compareToIgnoreCase(pw.shortName);
  432. }
  433. /**
  434. * returns true if backup of previous version of plugin exists
  435. */
  436. public boolean isDowngradable() {
  437. return getBackupFile().exists();
  438. }
  439. /**
  440. * Where is the backup file?
  441. */
  442. public File getBackupFile() {
  443. return new File(Hudson.getInstance().getRootDir(),"plugins/"+getShortName() + ".bak");
  444. }
  445. /**
  446. * returns the version of the backed up plugin,
  447. * or null if there's no back up.
  448. */
  449. public String getBackupVersion() {
  450. if (getBackupFile().exists()) {
  451. try {
  452. JarFile backupPlugin = new JarFile(getBackupFile());
  453. return backupPlugin.getManifest().getMainAttributes().getValue("Plugin-Version");
  454. } catch (IOException e) {
  455. LOGGER.log(WARNING, "Failed to get backup version ", e);
  456. return null;
  457. }
  458. } else {
  459. return null;
  460. }
  461. }
  462. //
  463. //
  464. // Action methods
  465. //
  466. //
  467. public HttpResponse doMakeEnabled() throws IOException {
  468. Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
  469. enable();
  470. return HttpResponses.ok();
  471. }
  472. public HttpResponse doMakeDisabled() throws IOException {
  473. Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
  474. disable();
  475. return HttpResponses.ok();
  476. }
  477. public HttpResponse doPin() throws IOException {
  478. Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
  479. new FileOutputStream(pinFile).close();
  480. return HttpResponses.ok();
  481. }
  482. public HttpResponse doUnpin() throws IOException {
  483. Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
  484. pinFile.delete();
  485. return HttpResponses.ok();
  486. }
  487. private static final Logger LOGGER = Logger.getLogger(PluginWrapper.class.getName());
  488. }