1 package hudson; 2 3 import hudson.util.IOException2; 4 import org.apache.tools.ant.BuildException; 5 import org.apache.tools.ant.Project; 6 import org.apache.tools.ant.taskdefs.Expand; 7 import org.apache.tools.ant.types.FileSet; 8 import org.kohsuke.stapler.StaplerRequest; 9 import org.kohsuke.stapler.StaplerResponse; 10 11 import java.io.BufferedReader ; 12 import java.io.File ; 13 import java.io.FileInputStream ; 14 import java.io.FileOutputStream ; 15 import java.io.FileReader ; 16 import java.io.FilenameFilter ; 17 import java.io.IOException ; 18 import java.io.OutputStream ; 19 import java.net.URL ; 20 import java.net.URLClassLoader ; 21 import java.util.ArrayList ; 22 import java.util.List ; 23 import java.util.jar.Manifest ; 24 import java.util.logging.Logger ; 25 26 48 public final class PluginWrapper { 49 53 private final Manifest manifest; 54 55 59 private Plugin plugin; 60 61 65 public final ClassLoader classLoader; 66 67 72 public final URL baseResourceURL; 73 74 78 private final File disableFile; 79 80 83 private final String shortName; 84 85 89 private final boolean active; 90 91 private final File archive; 92 93 private final List <Dependency> dependencies = new ArrayList <Dependency>(); 94 95 private static final class Dependency { 96 public final String shortName; 97 public final String version; 98 99 public Dependency(String s) { 100 int idx = s.indexOf(':'); 101 if(idx==-1) 102 throw new IllegalArgumentException ("Illegal dependency specifier "+s); 103 this.shortName = s.substring(0,idx); 104 this.version = s.substring(idx+1); 105 } 106 } 107 108 116 public PluginWrapper(PluginManager owner, File archive) throws IOException { 117 LOGGER.info("Loading plugin: "+archive); 118 this.archive = archive; 119 120 this.shortName = getShortName(archive); 121 122 boolean isLinked = archive.getName().endsWith(".hpl"); 123 124 File expandDir = null; 126 if(isLinked) { 127 String firstLine = new BufferedReader (new FileReader (archive)).readLine(); 129 if(firstLine.startsWith("Manifest-Version:")) { 130 } else { 132 archive = resolve(archive, firstLine); 134 } 135 FileInputStream in = new FileInputStream (archive); 137 try { 138 manifest = new Manifest (in); 139 } catch(IOException e) { 140 throw new IOException2("Failed to load "+archive,e); 141 } finally { 142 in.close(); 143 } 144 } else { 145 expandDir = new File (archive.getParentFile(), shortName); 146 explode(archive,expandDir); 147 148 File manifestFile = new File (expandDir,"META-INF/MANIFEST.MF"); 149 if(!manifestFile.exists()) { 150 throw new IOException ("Plugin installation failed. No manifest at "+manifestFile); 151 } 152 FileInputStream fin = new FileInputStream (manifestFile); 153 try { 154 manifest = new Manifest (fin); 155 } finally { 156 fin.close(); 157 } 158 } 159 160 163 List <URL > paths = new ArrayList <URL >(); 164 if(isLinked) { 165 parseClassPath(archive, paths, "Libraries", ","); 166 parseClassPath(archive, paths, "Class-Path", " +"); 168 this.baseResourceURL = resolve(archive, 169 manifest.getMainAttributes().getValue("Resource-Path")).toURL(); 170 } else { 171 File classes = new File (expandDir,"WEB-INF/classes"); 172 if(classes.exists()) 173 paths.add(classes.toURL()); 174 File lib = new File (expandDir,"WEB-INF/lib"); 175 File [] libs = lib.listFiles(JAR_FILTER); 176 if(libs!=null) { 177 for (File jar : libs) 178 paths.add(jar.toURL()); 179 } 180 181 this.baseResourceURL = expandDir.toURL(); 182 } 183 ClassLoader dependencyLoader = new DependencyClassLoader(getClass().getClassLoader(),owner); 184 this.classLoader = new URLClassLoader (paths.toArray(new URL [0]), dependencyLoader); 185 186 disableFile = new File (archive.getPath()+".disabled"); 187 if(disableFile.exists()) { 188 LOGGER.info("Plugin is disabled"); 189 this.active = false; 190 } else { 191 this.active = true; 192 } 193 194 String v = manifest.getMainAttributes().getValue("Plugin-Dependencies"); 196 if(v!=null) { 197 for(String s : v.split(",")) 198 dependencies.add(new Dependency(s)); 199 } 200 } 201 202 209 void load(PluginManager owner) throws IOException { 210 String className = manifest.getMainAttributes().getValue("Plugin-Class"); 211 if(className ==null) { 212 throw new IOException ("Plugin installation failed. No 'Plugin-Class' entry in the manifest of "+archive); 213 } 214 215 for (Dependency d : dependencies) { 217 if(owner.getPlugin(d.shortName)==null) 218 throw new IOException ("Dependency "+d.shortName+" doesn't exist"); 219 } 220 221 if(!active) 222 return; 223 224 ClassLoader old = Thread.currentThread().getContextClassLoader(); 227 Thread.currentThread().setContextClassLoader(classLoader); 228 try { 229 try { 230 Class clazz = classLoader.loadClass(className); 231 Object plugin = clazz.newInstance(); 232 if(!(plugin instanceof Plugin)) { 233 throw new IOException (className+" doesn't extend from hudson.Plugin"); 234 } 235 this.plugin = (Plugin)plugin; 236 this.plugin.wrapper = this; 237 } catch (ClassNotFoundException e) { 238 throw new IOException2("Unable to load " + className + " from " + archive,e); 239 } catch (IllegalAccessException e) { 240 throw new IOException2("Unable to create instance of " + className + " from " + archive,e); 241 } catch (InstantiationException e) { 242 throw new IOException2("Unable to create instance of " + className + " from " + archive,e); 243 } 244 245 try { 247 plugin.setServletContext(owner.context); 248 plugin.start(); 249 } catch(Throwable t) { 250 throw new IOException2("Failed to initialize",t); 252 } 253 } finally { 254 Thread.currentThread().setContextClassLoader(old); 255 } 256 } 257 258 private void parseClassPath(File archive, List <URL > paths, String attributeName, String separator) throws IOException { 259 String classPath = manifest.getMainAttributes().getValue(attributeName); 260 if(classPath==null) return; for (String s : classPath.split(separator)) { 262 File file = resolve(archive, s); 263 if(file.getName().contains("*")) { 264 FileSet fs = new FileSet(); 266 File dir = file.getParentFile(); 267 fs.setDir(dir); 268 fs.setIncludes(file.getName()); 269 for( String included : fs.getDirectoryScanner(new Project()).getIncludedFiles() ) { 270 paths.add(new File (dir,included).toURL()); 271 } 272 } else { 273 if(!file.exists()) 274 throw new IOException ("No such file: "+file); 275 paths.add(file.toURL()); 276 } 277 } 278 } 279 280 private static File resolve(File base, String relative) { 281 File rel = new File (relative); 282 if(rel.isAbsolute()) 283 return rel; 284 else 285 return new File (base.getParentFile(),relative); 286 } 287 288 291 public URL getIndexPage() { 292 return classLoader.getResource("index.jelly"); 293 } 294 295 298 public String getShortName() { 299 return shortName; 300 } 301 302 305 public Plugin getPlugin() { 306 return plugin; 307 } 308 309 @Override 310 public String toString() { 311 return "Plugin:" + getShortName(); 312 } 313 314 317 public String getLongName() { 318 String name = manifest.getMainAttributes().getValue("Long-PluginName"); 319 if(name!=null) return name; 320 return shortName; 321 } 322 323 326 public String getVersion() { 327 String v = manifest.getMainAttributes().getValue("Plugin-Version"); 328 if(v!=null) return v; 329 330 v = manifest.getMainAttributes().getValue("Implementation-Version"); 332 if(v!=null) return v; 333 334 return "???"; 335 } 336 337 340 private static String getShortName(File archive) { 341 String n = archive.getName(); 342 int idx = n.lastIndexOf('.'); 343 if(idx>=0) 344 n = n.substring(0,idx); 345 return n; 346 } 347 348 351 void stop() { 352 LOGGER.info("Stopping "+shortName); 353 try { 354 plugin.stop(); 355 } catch(Throwable t) { 356 System.err.println("Failed to shut down "+shortName); 357 System.err.println(t); 358 } 359 } 360 361 364 public void enable() throws IOException { 365 if(!disableFile.delete()) 366 throw new IOException ("Failed to delete "+disableFile); 367 } 368 369 372 public void disable() throws IOException { 373 OutputStream os = new FileOutputStream (disableFile); 375 os.close(); 376 } 377 378 381 public boolean isActive() { 382 return active; 383 } 384 385 389 public boolean isEnabled() { 390 return !disableFile.exists(); 391 } 392 393 396 private void explode(File archive, File destDir) throws IOException { 397 if(!destDir.exists()) 398 destDir.mkdirs(); 399 400 File explodeTime = new File (destDir,".timestamp"); 402 if(explodeTime.exists() && explodeTime.lastModified()>archive.lastModified()) 403 return; 405 LOGGER.info("Extracting "+archive); 406 407 Util.deleteContentsRecursive(destDir); 409 410 try { 411 Expand e = new Expand(); 412 e.setProject(new Project()); 413 e.setTaskType("unzip"); 414 e.setSrc(archive); 415 e.setDest(destDir); 416 e.execute(); 417 } catch (BuildException x) { 418 IOException ioe = new IOException ("Failed to expand " + archive); 419 ioe.initCause(x); 420 throw ioe; 421 } 422 423 Util.touch(explodeTime); 424 } 425 426 427 public void doMakeEnabled(StaplerRequest req, StaplerResponse rsp) throws IOException { 433 enable(); 434 rsp.setStatus(200); 435 } 436 public void doMakeDisabled(StaplerRequest req, StaplerResponse rsp) throws IOException { 437 disable(); 438 rsp.setStatus(200); 439 } 440 441 442 private static final Logger LOGGER = Logger.getLogger(PluginWrapper.class.getName()); 443 444 447 private static final FilenameFilter JAR_FILTER = new FilenameFilter () { 448 public boolean accept(File dir,String name) { 449 return name.endsWith(".jar"); 450 } 451 }; 452 453 456 final class DependencyClassLoader extends ClassLoader { 457 private final PluginManager manager; 458 459 public DependencyClassLoader(ClassLoader parent, PluginManager manager) { 460 super(parent); 461 this.manager = manager; 462 } 463 464 protected Class <?> findClass(String name) throws ClassNotFoundException { 465 for (Dependency dep : dependencies) { 466 PluginWrapper p = manager.getPlugin(dep.shortName); 467 if(p!=null) 468 try { 469 return p.classLoader.loadClass(name); 470 } catch (ClassNotFoundException _) { 471 } 473 } 474 475 throw new ClassNotFoundException (name); 476 } 477 478 } 480 } 481 | Popular Tags |