001 /** 002 * Licensed to the Apache Software Foundation (ASF) under one or more 003 * contributor license agreements. See the NOTICE file distributed with 004 * this work for additional information regarding copyright ownership. 005 * The ASF licenses this file to You under the Apache License, Version 2.0 006 * (the "License"); you may not use this file except in compliance with 007 * the License. You may obtain a copy of the License at 008 * 009 * http://www.apache.org/licenses/LICENSE-2.0 010 * 011 * Unless required by applicable law or agreed to in writing, software 012 * distributed under the License is distributed on an "AS IS" BASIS, 013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 014 * See the License for the specific language governing permissions and 015 * limitations under the License. 016 */ 017 018 package org.apache.geronimo.deployment; 019 020 import java.io.File; 021 import java.io.FilenameFilter; 022 import java.io.IOException; 023 import java.net.URI; 024 import java.net.URISyntaxException; 025 import java.util.ArrayList; 026 import java.util.Collection; 027 import java.util.Collections; 028 import java.util.Enumeration; 029 import java.util.Hashtable; 030 import java.util.Iterator; 031 import java.util.List; 032 import java.util.Properties; 033 import java.util.Set; 034 import java.util.jar.Attributes; 035 import java.util.jar.JarFile; 036 import java.util.jar.Manifest; 037 038 import javax.management.ObjectName; 039 040 import org.apache.commons.logging.Log; 041 import org.apache.commons.logging.LogFactory; 042 import org.apache.geronimo.common.DeploymentException; 043 import org.apache.geronimo.deployment.util.DeploymentUtil; 044 import org.apache.geronimo.gbean.AbstractName; 045 import org.apache.geronimo.gbean.AbstractNameQuery; 046 import org.apache.geronimo.gbean.GBeanInfo; 047 import org.apache.geronimo.gbean.GBeanInfoBuilder; 048 import org.apache.geronimo.gbean.GBeanLifecycle; 049 import org.apache.geronimo.kernel.GBeanNotFoundException; 050 import org.apache.geronimo.kernel.Kernel; 051 import org.apache.geronimo.kernel.config.Configuration; 052 import org.apache.geronimo.kernel.config.ConfigurationData; 053 import org.apache.geronimo.kernel.config.ConfigurationManager; 054 import org.apache.geronimo.kernel.config.ConfigurationStore; 055 import org.apache.geronimo.kernel.config.ConfigurationUtil; 056 import org.apache.geronimo.kernel.config.InvalidConfigException; 057 import org.apache.geronimo.kernel.config.DeploymentWatcher; 058 import org.apache.geronimo.kernel.repository.Artifact; 059 import org.apache.geronimo.kernel.repository.ArtifactResolver; 060 import org.apache.geronimo.system.configuration.ExecutableConfigurationUtil; 061 import org.apache.geronimo.system.main.CommandLineManifest; 062 063 /** 064 * GBean that knows how to deploy modules (by consulting available module builders) 065 * 066 * @version $Rev: 706640 $ $Date: 2008-10-21 14:44:05 +0000 (Tue, 21 Oct 2008) $ 067 */ 068 public class Deployer implements GBeanLifecycle { 069 private static final Log log = LogFactory.getLog(Deployer.class); 070 private final int REAPER_INTERVAL = 60 * 1000; 071 private DeployerReaper reaper; 072 private final String remoteDeployAddress; 073 private final Collection builders; 074 private final Collection stores; 075 private final Collection watchers; 076 private final ArtifactResolver artifactResolver; 077 private final Kernel kernel; 078 079 public Deployer(String remoteDeployAddress, Collection builders, Collection stores, Collection watchers, Kernel kernel) { 080 this(remoteDeployAddress, builders, stores, watchers, getArtifactResolver(kernel), kernel); 081 } 082 083 private static ArtifactResolver getArtifactResolver(Kernel kernel) { 084 ConfigurationManager configurationManager = ConfigurationUtil.getConfigurationManager(kernel); 085 return configurationManager.getArtifactResolver(); 086 } 087 088 public Deployer(String remoteDeployAddress, Collection builders, Collection stores, Collection watchers, ArtifactResolver artifactResolver, Kernel kernel) { 089 this.remoteDeployAddress = remoteDeployAddress; 090 this.builders = builders; 091 this.stores = stores; 092 this.watchers = watchers; 093 this.artifactResolver = artifactResolver; 094 this.kernel = kernel; 095 096 // Create and start the reaper... 097 this.reaper = new DeployerReaper(REAPER_INTERVAL); 098 099 } 100 101 public List deploy(boolean inPlace, File moduleFile, File planFile) throws DeploymentException { 102 return deploy(inPlace, moduleFile, planFile, null); 103 } 104 105 public List deploy(boolean inPlace, File moduleFile, File planFile, String targetConfigStore) throws DeploymentException { 106 File originalModuleFile = moduleFile; 107 File tmpDir = null; 108 if (moduleFile != null && !moduleFile.isDirectory()) { 109 // todo jar url handling with Sun's VM on Windows leaves a lock on the module file preventing rebuilds 110 // to address this we use a gross hack and copy the file to a temporary directory 111 // the lock on the file will prevent that being deleted properly until the URLJarFile has 112 // been GC'ed. 113 boolean cleanup = true; 114 try { 115 tmpDir = File.createTempFile("geronimo-deployer", ".tmpdir"); 116 tmpDir.delete(); 117 tmpDir.mkdir(); 118 File tmpFile = new File(tmpDir, moduleFile.getName()); 119 DeploymentUtil.copyFile(moduleFile, tmpFile); 120 moduleFile = tmpFile; 121 cleanup = false; 122 } catch (IOException e) { 123 throw new DeploymentException(e); 124 } finally { 125 // If an Exception is thrown in the try block above, we will need to cleanup here. 126 if(cleanup && tmpDir != null && !DeploymentUtil.recursiveDelete(tmpDir)) { 127 reaper.delete(tmpDir.getAbsolutePath(), "delete"); 128 } 129 } 130 } 131 132 try { 133 return deploy(inPlace, moduleFile, planFile, null, true, null, null, null, null, null, null, null, targetConfigStore); 134 } catch (DeploymentException e) { 135 log.debug("Deployment failed: plan=" + planFile + ", module=" + originalModuleFile, e); 136 throw e.cleanse(); 137 } finally { 138 if (tmpDir != null) { 139 if (!DeploymentUtil.recursiveDelete(tmpDir)) { 140 reaper.delete(tmpDir.getAbsolutePath(), "delete"); 141 } 142 } 143 } 144 } 145 146 /** 147 * Gets a URL that a remote deploy client can use to upload files to the 148 * server. Looks up a remote deploy web application by searching for a 149 * particular GBean and figuring out a reference to the web application 150 * based on that. Then constructs a URL pointing to that web application 151 * based on available connectors for the web container and the context 152 * root for the web application. 153 * 154 * @return The URL that clients should use for deployment file uploads. 155 */ 156 public String getRemoteDeployUploadURL() { 157 // Get the token GBean from the remote deployment configuration 158 Set set = kernel.listGBeans(new AbstractNameQuery("org.apache.geronimo.deployment.remote.RemoteDeployToken")); 159 if (set.size() == 0) { 160 return null; 161 } 162 AbstractName token = (AbstractName) set.iterator().next(); 163 // Identify the parent configuration for that GBean 164 set = kernel.getDependencyManager().getParents(token); 165 ObjectName config = null; 166 for (Iterator it = set.iterator(); it.hasNext();) { 167 AbstractName name = (AbstractName) it.next(); 168 if (Configuration.isConfigurationObjectName(name.getObjectName())) { 169 config = name.getObjectName(); 170 break; 171 } 172 } 173 if (config == null) { 174 log.warn("Unable to find remote deployment configuration; is the remote deploy web application running?"); 175 return null; 176 } 177 // Generate the URL based on the remote deployment configuration 178 Hashtable hash = new Hashtable(); 179 hash.put("J2EEApplication", token.getObjectName().getKeyProperty("J2EEApplication")); 180 hash.put("j2eeType", "WebModule"); 181 try { 182 hash.put("name", Configuration.getConfigurationID(config).toString()); 183 Set names = kernel.listGBeans(new AbstractNameQuery(null, hash)); 184 if (names.size() != 1) { 185 log.error("Unable to look up remote deploy upload URL"); 186 return null; 187 } 188 AbstractName module = (AbstractName) names.iterator().next(); 189 String contextPath = (String) kernel.getAttribute(module, "contextPath"); 190 if (null == contextPath) { 191 throw new IllegalStateException("Cannot find contextPath attribute for [" + module + "]"); 192 } 193 String temp = remoteDeployAddress + "/" + contextPath + "/upload"; 194 return URI.create(temp).normalize().toString(); 195 } catch (Exception e) { 196 log.error("Unable to look up remote deploy upload URL", e); 197 return null; 198 } 199 } 200 201 public List deploy(boolean inPlace, 202 File moduleFile, 203 File planFile, 204 File targetFile, 205 boolean install, 206 String mainClass, 207 String mainGBean, String mainMethod, String manifestConfigurations, String classPath, 208 String endorsedDirs, 209 String extensionDirs, 210 String targetConfigurationStore) throws DeploymentException { 211 if (planFile == null && moduleFile == null) { 212 throw new DeploymentException("No plan or module specified"); 213 } else if (stores.isEmpty()) { 214 throw new DeploymentException("No ConfigurationStores!"); 215 } 216 validatePlanFile(planFile); 217 218 JarFile module = getModule(inPlace, moduleFile); 219 220 ModuleIDBuilder idBuilder = new ModuleIDBuilder(); 221 try { 222 Object plan = null; 223 ConfigurationBuilder builder = null; 224 for (Iterator i = builders.iterator(); i.hasNext();) { 225 ConfigurationBuilder candidate = (ConfigurationBuilder) i.next(); 226 plan = candidate.getDeploymentPlan(planFile, module, idBuilder); 227 if (plan != null) { 228 builder = candidate; 229 break; 230 } 231 } 232 if (builder == null) { 233 throw new DeploymentException("Cannot deploy the requested application module because no deployer is able to handle it. " + 234 " This can happen if you have omitted the J2EE deployment descriptor, disabled a deployer module, or if, for example, you are trying to deploy an" + 235 " EJB module on a minimal Geronimo server that does not have EJB support installed. (" + 236 (planFile == null ? "" : "planFile=" + planFile.getAbsolutePath()) + 237 (moduleFile == null ? "" : (planFile == null ? "" : ", ") + "moduleFile=" + moduleFile.getAbsolutePath()) + ")"); 238 } 239 240 Artifact configID = getConfigID(module, idBuilder, plan, builder); 241 242 // create the manifest 243 Manifest manifest = createManifest(mainClass, 244 mainGBean, 245 mainMethod, 246 manifestConfigurations, 247 classPath, 248 endorsedDirs, 249 extensionDirs); 250 251 ConfigurationStore store = getConfigurationStore(targetConfigurationStore); 252 253 // It's our responsibility to close this context, once we're done with it... 254 DeploymentContext context = builder.buildConfiguration(inPlace, configID, plan, module, stores, artifactResolver, store); 255 256 return install(targetFile, install, manifest, store, context); 257 } catch (Throwable e) { 258 //TODO not clear all errors will result in total cleanup 259 // File configurationDir = configurationData.getConfigurationDir(); 260 // if (!DeploymentUtil.recursiveDelete(configurationDir)) { 261 // pendingDeletionIndex.setProperty(configurationDir.getName(), new String("delete")); 262 // log.debug("Queued deployment directory to be reaped " + configurationDir); 263 // } 264 // if (targetFile != null) { 265 // targetFile.delete(); 266 // } 267 268 if (e instanceof Error) { 269 log.error("Deployment failed due to ", e); 270 throw (Error) e; 271 } else if (e instanceof DeploymentException) { 272 throw (DeploymentException) e; 273 } else if (e instanceof Exception) { 274 log.error("Deployment failed due to ", e); 275 throw new DeploymentException(e); 276 } 277 throw new Error(e); 278 } finally { 279 DeploymentUtil.close(module); 280 } 281 } 282 283 private ConfigurationStore getConfigurationStore(String targetConfigurationStore) 284 throws URISyntaxException, GBeanNotFoundException { 285 if (targetConfigurationStore != null) { 286 AbstractName targetStoreName = new AbstractName(new URI(targetConfigurationStore)); 287 return (ConfigurationStore) kernel.getGBean(targetStoreName); 288 } else { 289 return (ConfigurationStore) stores.iterator().next(); 290 } 291 } 292 293 private Artifact getConfigID(JarFile module, 294 ModuleIDBuilder idBuilder, 295 Object plan, 296 ConfigurationBuilder builder) throws IOException, DeploymentException, InvalidConfigException { 297 Artifact configID = builder.getConfigurationID(plan, module, idBuilder); 298 // If the Config ID isn't fully resolved, populate it with defaults 299 if (!configID.isResolved()) { 300 configID = idBuilder.resolve(configID, "car"); 301 } 302 303 // Make sure this configuration doesn't already exist 304 try { 305 kernel.getGBeanState(Configuration.getConfigurationAbstractName(configID)); 306 throw new DeploymentException("Module " + configID + " already exists in the server. Try to undeploy it first or use the redeploy command."); 307 } catch (GBeanNotFoundException e) { 308 // this is good 309 } 310 return configID; 311 } 312 313 private List install(File targetFile, 314 boolean install, 315 Manifest manifest, 316 ConfigurationStore store, 317 DeploymentContext context) throws DeploymentException, IOException, Throwable { 318 List<ConfigurationData> configurations = new ArrayList<ConfigurationData>(); 319 configurations.add(context.getConfigurationData()); 320 configurations.addAll(context.getAdditionalDeployment()); 321 322 if (configurations.isEmpty()) { 323 throw new DeploymentException("Deployer did not create any configurations"); 324 } 325 326 boolean configsCleanupRequired = false; 327 328 // Set TCCL to the classloader for the configuration being deployed 329 // so that any static blocks invoked during the loading of classes 330 // during serialization of the configuration have the correct TCCL 331 // ( a TCCL that is consistent with what is set when the same 332 // classes are loaded when the configuration is started. 333 Thread thread = Thread.currentThread(); 334 ClassLoader oldCl = thread.getContextClassLoader(); 335 thread.setContextClassLoader( context.getConfiguration().getConfigurationClassLoader()); 336 try { 337 if (targetFile != null) { 338 if (configurations.size() > 1) { 339 throw new DeploymentException("Deployer created more than one configuration"); 340 } 341 ConfigurationData configurationData = (ConfigurationData) configurations.get(0); 342 ExecutableConfigurationUtil.createExecutableConfiguration(configurationData, manifest, targetFile); 343 } 344 if (install) { 345 List deployedURIs = new ArrayList(); 346 for (Iterator iterator = configurations.iterator(); iterator.hasNext();) { 347 ConfigurationData configurationData = (ConfigurationData) iterator.next(); 348 store.install(configurationData); 349 deployedURIs.add(configurationData.getId().toString()); 350 } 351 notifyWatchers(deployedURIs); 352 return deployedURIs; 353 } else { 354 configsCleanupRequired = true; 355 return Collections.EMPTY_LIST; 356 } 357 } catch (DeploymentException e) { 358 configsCleanupRequired = true; 359 throw e; 360 } catch (IOException e) { 361 configsCleanupRequired = true; 362 throw e; 363 } catch (InvalidConfigException e) { 364 configsCleanupRequired = true; 365 // unlikely as we just built this 366 throw new DeploymentException(e); 367 } catch (Throwable e) { 368 // Could get here if serialization of the configuration failed (GERONIMO-1996) 369 configsCleanupRequired = true; 370 throw e; 371 } finally { 372 thread.setContextClassLoader(oldCl); 373 if (context != null) { 374 context.close(); 375 } 376 if (configsCleanupRequired) { 377 // We do this after context is closed so the module jar isn't open 378 cleanupConfigurations(configurations); 379 } 380 } 381 } 382 383 private Manifest createManifest(String mainClass, 384 String mainGBean, 385 String mainMethod, 386 String manifestConfigurations, 387 String classPath, 388 String endorsedDirs, 389 String extensionDirs) { 390 if (mainClass == null) { 391 return null; 392 } 393 Manifest manifest = new Manifest(); 394 Attributes mainAttributes = manifest.getMainAttributes(); 395 mainAttributes.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0"); 396 if (mainClass != null) { 397 mainAttributes.putValue(Attributes.Name.MAIN_CLASS.toString(), mainClass); 398 } 399 if (mainGBean != null) { 400 mainAttributes.putValue(CommandLineManifest.MAIN_GBEAN.toString(), mainGBean); 401 } 402 if (mainMethod != null) { 403 mainAttributes.putValue(CommandLineManifest.MAIN_METHOD.toString(), mainMethod); 404 } 405 if (manifestConfigurations != null) { 406 mainAttributes.putValue(CommandLineManifest.CONFIGURATIONS.toString(), manifestConfigurations); 407 } 408 if (classPath != null) { 409 mainAttributes.putValue(Attributes.Name.CLASS_PATH.toString(), classPath); 410 } 411 if (endorsedDirs != null) { 412 mainAttributes.putValue(CommandLineManifest.ENDORSED_DIRS.toString(), endorsedDirs); 413 } 414 if (extensionDirs != null) { 415 mainAttributes.putValue(CommandLineManifest.EXTENSION_DIRS.toString(), extensionDirs); 416 } 417 return manifest; 418 } 419 420 private JarFile getModule(boolean inPlace, File moduleFile) throws DeploymentException { 421 JarFile module = null; 422 if (moduleFile != null) { 423 if (inPlace && !moduleFile.isDirectory()) { 424 throw new DeploymentException("In place deployment is not allowed for packed module"); 425 } 426 if (!moduleFile.exists()) { 427 throw new DeploymentException("Module file does not exist: " + moduleFile.getAbsolutePath()); 428 } 429 try { 430 module = DeploymentUtil.createJarFile(moduleFile); 431 } catch (IOException e) { 432 throw new DeploymentException("Cound not open module file: " + moduleFile.getAbsolutePath(), e); 433 } 434 } 435 return module; 436 } 437 438 private void validatePlanFile(File planFile) throws DeploymentException { 439 if (planFile != null) { 440 if (!planFile.exists()) { 441 throw new DeploymentException("Plan file does not exist: " + planFile.getAbsolutePath()); 442 } 443 if (!planFile.isFile()) { 444 throw new DeploymentException("Plan file is not a regular file: " + planFile.getAbsolutePath()); 445 } 446 } 447 } 448 449 private void notifyWatchers(List list) { 450 Artifact[] arts = new Artifact[list.size()]; 451 for (int i = 0; i < list.size(); i++) { 452 String s = (String) list.get(i); 453 arts[i] = Artifact.create(s); 454 } 455 for (Iterator it = watchers.iterator(); it.hasNext();) { 456 DeploymentWatcher watcher = (DeploymentWatcher) it.next(); 457 for (int i = 0; i < arts.length; i++) { 458 Artifact art = arts[i]; 459 watcher.deployed(art); 460 } 461 } 462 } 463 464 private void cleanupConfigurations(List configurations) { 465 for (Iterator iterator = configurations.iterator(); iterator.hasNext();) { 466 ConfigurationData configurationData = (ConfigurationData) iterator.next(); 467 File configurationDir = configurationData.getConfigurationDir(); 468 if (!DeploymentUtil.recursiveDelete(configurationDir)) { 469 reaper.delete(configurationDir.getAbsolutePath(), "delete"); 470 } 471 } 472 } 473 474 public void doStart() throws Exception { 475 } 476 477 public void doFail() { 478 if (reaper != null) { 479 reaper.close(); 480 } 481 } 482 483 public void doStop() throws Exception { 484 } 485 486 /** 487 * Thread to cleanup unused temporary Deployer directories (and files). 488 * On Windows, open files can't be deleted. Until MultiParentClassLoaders 489 * are GC'ed, we won't be able to delete Config Store directories/files. 490 */ 491 class DeployerReaper implements Runnable { 492 private final int reaperInterval; 493 private final Properties pendingDeletionIndex = new Properties(); 494 private volatile boolean done = false; 495 private Thread thread; 496 497 public DeployerReaper(int reaperInterval) { 498 this.reaperInterval = reaperInterval; 499 } 500 501 public void delete(String dir, String type) { 502 pendingDeletionIndex.setProperty(dir, type); 503 log.debug("Queued deployment directory to be reaped " + dir); 504 startThread(); 505 } 506 507 private synchronized void startThread() { 508 if (this.thread == null) { 509 this.thread = new Thread(this, "Geronimo Config Store Reaper"); 510 this.thread.setDaemon(true); 511 this.thread.start(); 512 } 513 } 514 515 public void close() { 516 this.done = true; 517 } 518 519 public void run() { 520 log.debug("ConfigStoreReaper started"); 521 522 // DeployerReaper in offline deployment does not get a chance to reap the temporary 523 // directories. Reap any temporary directories left behind by previous runs here. 524 reapBacklog(); 525 526 while (!done) { 527 try { 528 Thread.sleep(reaperInterval); 529 } catch (InterruptedException e) { 530 continue; 531 } 532 reap(); 533 } 534 } 535 536 /** 537 * Reap any temporary directories left behind by previous runs. 538 */ 539 private void reapBacklog() { 540 try { 541 File tempFile = File.createTempFile("geronimo-deployer", ".tmpdir"); 542 File tempDir = tempFile.getParentFile(); 543 tempFile.delete(); 544 String[] backlog = tempDir.list(new FilenameFilter(){ 545 public boolean accept(File dir, String name) { 546 return name.startsWith("geronimo-deployer") && name.endsWith(".tmpdir") && new File(dir, name).isDirectory(); 547 }}); 548 for(String dir: backlog) { 549 File deleteDir = new File(tempDir, dir); 550 DeploymentUtil.recursiveDelete(deleteDir); 551 log.debug("Reaped deployment directory from previous runs " + deleteDir); 552 } 553 } catch (IOException ignored) { 554 } 555 } 556 557 /** 558 * For every directory in the pendingDeletionIndex, attempt to delete all 559 * sub-directories and files. 560 */ 561 public void reap() { 562 // return, if there's nothing to do 563 if (pendingDeletionIndex.size() == 0) 564 return; 565 // Otherwise, attempt to delete all of the directories 566 Enumeration list = pendingDeletionIndex.propertyNames(); 567 while (list.hasMoreElements()) { 568 String dirName = (String) list.nextElement(); 569 File deleteDir = new File(dirName); 570 571 if (DeploymentUtil.recursiveDelete(deleteDir)) { 572 pendingDeletionIndex.remove(dirName); 573 log.debug("Reaped deployment directory " + deleteDir); 574 } 575 } 576 } 577 } 578 579 public static final GBeanInfo GBEAN_INFO; 580 581 private static final String DEPLOYER = "Deployer"; 582 583 static { 584 GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic(Deployer.class, DEPLOYER); 585 586 infoFactory.addAttribute("kernel", Kernel.class, false); 587 infoFactory.addAttribute("remoteDeployAddress", String.class, true, true); 588 infoFactory.addAttribute("remoteDeployUploadURL", String.class, false); 589 infoFactory.addOperation("deploy", new Class[]{boolean.class, File.class, File.class}); 590 infoFactory.addOperation("deploy", new Class[]{boolean.class, File.class, File.class, String.class}); 591 infoFactory.addOperation("deploy", new Class[]{boolean.class, File.class, File.class, File.class, boolean.class, String.class, String.class, String.class, String.class, String.class, String.class, String.class, String.class}); 592 593 infoFactory.addReference("Builders", ConfigurationBuilder.class, "ConfigBuilder"); 594 infoFactory.addReference("Store", ConfigurationStore.class, "ConfigurationStore"); 595 infoFactory.addReference("Watchers", DeploymentWatcher.class); 596 597 infoFactory.setConstructor(new String[]{"remoteDeployAddress", "Builders", "Store", "Watchers", "kernel"}); 598 599 GBEAN_INFO = infoFactory.getBeanInfo(); 600 } 601 602 public static GBeanInfo getGBeanInfo() { 603 return GBEAN_INFO; 604 } 605 606 }