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.IOException;
022 import java.net.URI;
023 import java.util.ArrayList;
024 import java.util.Collection;
025 import java.util.Collections;
026 import java.util.Enumeration;
027 import java.util.Hashtable;
028 import java.util.Iterator;
029 import java.util.List;
030 import java.util.Properties;
031 import java.util.Set;
032 import java.util.jar.Attributes;
033 import java.util.jar.JarFile;
034 import java.util.jar.Manifest;
035
036 import javax.management.ObjectName;
037
038 import org.apache.commons.logging.Log;
039 import org.apache.commons.logging.LogFactory;
040 import org.apache.geronimo.common.DeploymentException;
041 import org.apache.geronimo.deployment.util.DeploymentUtil;
042 import org.apache.geronimo.gbean.AbstractName;
043 import org.apache.geronimo.gbean.AbstractNameQuery;
044 import org.apache.geronimo.gbean.GBeanInfo;
045 import org.apache.geronimo.gbean.GBeanInfoBuilder;
046 import org.apache.geronimo.kernel.GBeanNotFoundException;
047 import org.apache.geronimo.kernel.Kernel;
048 import org.apache.geronimo.kernel.config.Configuration;
049 import org.apache.geronimo.kernel.config.ConfigurationData;
050 import org.apache.geronimo.kernel.config.ConfigurationManager;
051 import org.apache.geronimo.kernel.config.ConfigurationStore;
052 import org.apache.geronimo.kernel.config.ConfigurationUtil;
053 import org.apache.geronimo.kernel.config.InvalidConfigException;
054 import org.apache.geronimo.kernel.config.DeploymentWatcher;
055 import org.apache.geronimo.kernel.repository.Artifact;
056 import org.apache.geronimo.kernel.repository.ArtifactResolver;
057 import org.apache.geronimo.system.configuration.ExecutableConfigurationUtil;
058 import org.apache.geronimo.system.main.CommandLineManifest;
059
060 /**
061 * GBean that knows how to deploy modules (by consulting available module builders)
062 *
063 * @version $Rev: 562021 $ $Date: 2007-08-02 01:51:18 -0400 (Thu, 02 Aug 2007) $
064 */
065 public class Deployer {
066 private static final Log log = LogFactory.getLog(Deployer.class);
067 private final int REAPER_INTERVAL = 60 * 1000;
068 private final Properties pendingDeletionIndex = new Properties();
069 private DeployerReaper reaper;
070 private final String remoteDeployAddress;
071 private final Collection builders;
072 private final Collection stores;
073 private final Collection watchers;
074 private final ArtifactResolver artifactResolver;
075 private final Kernel kernel;
076
077 public Deployer(String remoteDeployAddress, Collection builders, Collection stores, Collection watchers, Kernel kernel) {
078 this(remoteDeployAddress, builders, stores, watchers, getArtifactResolver(kernel), kernel);
079 }
080
081 private static ArtifactResolver getArtifactResolver(Kernel kernel) {
082 ConfigurationManager configurationManager = ConfigurationUtil.getConfigurationManager(kernel);
083 return configurationManager.getArtifactResolver();
084 }
085
086 public Deployer(String remoteDeployAddress, Collection builders, Collection stores, Collection watchers, ArtifactResolver artifactResolver, Kernel kernel) {
087 this.remoteDeployAddress = remoteDeployAddress;
088 this.builders = builders;
089 this.stores = stores;
090 this.watchers = watchers;
091 this.artifactResolver = artifactResolver;
092 this.kernel = kernel;
093
094 // Create and start the reaper...
095 this.reaper = new DeployerReaper(REAPER_INTERVAL);
096 Thread t = new Thread(reaper, "Geronimo Config Store Reaper");
097 t.setDaemon(true);
098 t.start();
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 try {
114 tmpDir = File.createTempFile("geronimo-deployer", ".tmpdir");
115 tmpDir.delete();
116 tmpDir.mkdir();
117 File tmpFile = new File(tmpDir, moduleFile.getName());
118 DeploymentUtil.copyFile(moduleFile, tmpFile);
119 moduleFile = tmpFile;
120 } catch (IOException e) {
121 throw new DeploymentException(e);
122 }
123 }
124
125 try {
126 return deploy(inPlace, moduleFile, planFile, null, true, null, null, null, null, null, null, null, targetConfigStore);
127 } catch (DeploymentException e) {
128 log.debug("Deployment failed: plan=" + planFile + ", module=" + originalModuleFile, e);
129 throw e.cleanse();
130 } finally {
131 if (tmpDir != null) {
132 if (!DeploymentUtil.recursiveDelete(tmpDir)) {
133 pendingDeletionIndex.setProperty(tmpDir.getName(), "delete");
134 }
135 }
136 }
137 }
138
139 /**
140 * Gets a URL that a remote deploy client can use to upload files to the
141 * server. Looks up a remote deploy web application by searching for a
142 * particular GBean and figuring out a reference to the web application
143 * based on that. Then constructs a URL pointing to that web application
144 * based on available connectors for the web container and the context
145 * root for the web application.
146 *
147 * @return The URL that clients should use for deployment file uploads.
148 */
149 public String getRemoteDeployUploadURL() {
150 // Get the token GBean from the remote deployment configuration
151 Set set = kernel.listGBeans(new AbstractNameQuery("org.apache.geronimo.deployment.remote.RemoteDeployToken"));
152 if (set.size() == 0) {
153 return null;
154 }
155 AbstractName token = (AbstractName) set.iterator().next();
156 // Identify the parent configuration for that GBean
157 set = kernel.getDependencyManager().getParents(token);
158 ObjectName config = null;
159 for (Iterator it = set.iterator(); it.hasNext();) {
160 AbstractName name = (AbstractName) it.next();
161 if (Configuration.isConfigurationObjectName(name.getObjectName())) {
162 config = name.getObjectName();
163 break;
164 }
165 }
166 if (config == null) {
167 log.warn("Unable to find remote deployment configuration; is the remote deploy web application running?");
168 return null;
169 }
170 // Generate the URL based on the remote deployment configuration
171 Hashtable hash = new Hashtable();
172 hash.put("J2EEApplication", token.getObjectName().getKeyProperty("J2EEApplication"));
173 hash.put("j2eeType", "WebModule");
174 try {
175 hash.put("name", Configuration.getConfigurationID(config).toString());
176 Set names = kernel.listGBeans(new AbstractNameQuery(null, hash));
177 if (names.size() != 1) {
178 log.error("Unable to look up remote deploy upload URL");
179 return null;
180 }
181 AbstractName module = (AbstractName) names.iterator().next();
182 return remoteDeployAddress + "/" + kernel.getAttribute(module, "contextPath") + "/upload";
183 } catch (Exception e) {
184 log.error("Unable to look up remote deploy upload URL", e);
185 return null;
186 }
187 }
188
189 public List deploy(boolean inPlace,
190 File moduleFile,
191 File planFile,
192 File targetFile,
193 boolean install,
194 String mainClass,
195 String mainGBean, String mainMethod, String manifestConfigurations, String classPath,
196 String endorsedDirs,
197 String extensionDirs,
198 String targetConfigurationStore) throws DeploymentException {
199 if (planFile == null && moduleFile == null) {
200 throw new DeploymentException("No plan or module specified");
201 }
202
203 if (planFile != null) {
204 if (!planFile.exists()) {
205 throw new DeploymentException("Plan file does not exist: " + planFile.getAbsolutePath());
206 }
207 if (!planFile.isFile()) {
208 throw new DeploymentException("Plan file is not a regular file: " + planFile.getAbsolutePath());
209 }
210 }
211
212 JarFile module = null;
213 if (moduleFile != null) {
214 if (inPlace && !moduleFile.isDirectory()) {
215 throw new DeploymentException("In place deployment is not allowed for packed module");
216 }
217 if (!moduleFile.exists()) {
218 throw new DeploymentException("Module file does not exist: " + moduleFile.getAbsolutePath());
219 }
220 try {
221 module = DeploymentUtil.createJarFile(moduleFile);
222 } catch (IOException e) {
223 throw new DeploymentException("Cound not open module file: " + moduleFile.getAbsolutePath(), e);
224 }
225 }
226
227 // File configurationDir = null;
228 ModuleIDBuilder idBuilder = new ModuleIDBuilder();
229 try {
230 Object plan = null;
231 ConfigurationBuilder builder = null;
232 for (Iterator i = builders.iterator(); i.hasNext();) {
233 ConfigurationBuilder candidate = (ConfigurationBuilder) i.next();
234 plan = candidate.getDeploymentPlan(planFile, module, idBuilder);
235 if (plan != null) {
236 builder = candidate;
237 break;
238 }
239 }
240 if (builder == null) {
241 throw new DeploymentException("Cannot deploy the requested application module because no deployer is able to handle it. " +
242 " 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" +
243 " EJB module on a minimal Geronimo server that does not have EJB support installed. (" +
244 (planFile == null ? "" : "planFile=" + planFile.getAbsolutePath()) +
245 (moduleFile == null ? "" : (planFile == null ? "" : ", ") + "moduleFile=" + moduleFile.getAbsolutePath()) + ")");
246 }
247
248 Artifact configID = builder.getConfigurationID(plan, module, idBuilder);
249 // If the Config ID isn't fully resolved, populate it with defaults
250 if (!configID.isResolved()) {
251 configID = idBuilder.resolve(configID, "car");
252 }
253 // Make sure this configuration doesn't already exist
254 try {
255 kernel.getGBeanState(Configuration.getConfigurationAbstractName(configID));
256 throw new DeploymentException("Module " + configID + " already exists in the server. Try to undeploy it first or use the redeploy command.");
257 } catch (GBeanNotFoundException e) {
258 // this is good
259 }
260
261 // create the manifest
262 Manifest manifest;
263 if (mainClass != null) {
264 manifest = new Manifest();
265 Attributes mainAttributes = manifest.getMainAttributes();
266 mainAttributes.putValue(Attributes.Name.MANIFEST_VERSION.toString(), "1.0");
267 if (mainClass != null) {
268 mainAttributes.putValue(Attributes.Name.MAIN_CLASS.toString(), mainClass);
269 }
270 if (mainGBean != null) {
271 mainAttributes.putValue(CommandLineManifest.MAIN_GBEAN.toString(), mainGBean);
272 }
273 if (mainMethod != null) {
274 mainAttributes.putValue(CommandLineManifest.MAIN_METHOD.toString(), mainMethod);
275 }
276 if (manifestConfigurations != null) {
277 mainAttributes.putValue(CommandLineManifest.CONFIGURATIONS.toString(), manifestConfigurations);
278 }
279 if (classPath != null) {
280 mainAttributes.putValue(Attributes.Name.CLASS_PATH.toString(), classPath);
281 }
282 if (endorsedDirs != null) {
283 mainAttributes.putValue(CommandLineManifest.ENDORSED_DIRS.toString(), endorsedDirs);
284 }
285 if (extensionDirs != null) {
286 mainAttributes.putValue(CommandLineManifest.EXTENSION_DIRS.toString(), extensionDirs);
287 }
288 } else {
289 manifest = null;
290 }
291
292 if (stores.isEmpty()) {
293 throw new DeploymentException("No ConfigurationStores!");
294 }
295 ConfigurationStore store;
296 if (targetConfigurationStore != null) {
297 AbstractName targetStoreName = new AbstractName(new URI(targetConfigurationStore));
298 store = (ConfigurationStore) kernel.getGBean(targetStoreName);
299 } else {
300 store = (ConfigurationStore) stores.iterator().next();
301 }
302
303 // It's our responsibility to close this context, once we're done with it...
304 DeploymentContext context = builder.buildConfiguration(inPlace, configID, plan, module, stores, artifactResolver, store);
305 List configurations = new ArrayList();
306 boolean configsCleanupRequired = false;
307 configurations.add(context.getConfigurationData());
308 configurations.addAll(context.getAdditionalDeployment());
309
310 if (configurations.isEmpty()) {
311 throw new DeploymentException("Deployer did not create any configurations");
312 }
313
314 // Set TCCL to the classloader for the configuration being deployed
315 // so that any static blocks invoked during the loading of classes
316 // during serialization of the configuration have the correct TCCL
317 // ( a TCCL that is consistent with what is set when the same
318 // classes are loaded when the configuration is started.
319 Thread thread = Thread.currentThread();
320 ClassLoader oldCl = thread.getContextClassLoader();
321 thread.setContextClassLoader( context.getConfiguration().getConfigurationClassLoader());
322 try {
323 if (targetFile != null) {
324 if (configurations.size() > 1) {
325 throw new DeploymentException("Deployer created more than one configuration");
326 }
327 ConfigurationData configurationData = (ConfigurationData) configurations.get(0);
328 ExecutableConfigurationUtil.createExecutableConfiguration(configurationData, manifest, targetFile);
329 }
330 if (install) {
331 List deployedURIs = new ArrayList();
332 for (Iterator iterator = configurations.iterator(); iterator.hasNext();) {
333 ConfigurationData configurationData = (ConfigurationData) iterator.next();
334 store.install(configurationData);
335 deployedURIs.add(configurationData.getId().toString());
336 }
337 notifyWatchers(deployedURIs);
338 return deployedURIs;
339 } else {
340 configsCleanupRequired = true;
341 return Collections.EMPTY_LIST;
342 }
343 } catch (DeploymentException e) {
344 configsCleanupRequired = true;
345 throw e;
346 } catch (IOException e) {
347 configsCleanupRequired = true;
348 throw e;
349 } catch (InvalidConfigException e) {
350 configsCleanupRequired = true;
351 // unlikely as we just built this
352 throw new DeploymentException(e);
353 } catch (Throwable e) {
354 // Could get here if serialization of the configuration failed (GERONIMO-1996)
355 configsCleanupRequired = true;
356 throw e;
357 } finally {
358 thread.setContextClassLoader(oldCl);
359 if (context != null) {
360 context.close();
361 }
362 if (configsCleanupRequired) {
363 // We do this after context is closed so the module jar isn't open
364 cleanupConfigurations(configurations);
365 }
366 }
367 } catch (Throwable e) {
368 //TODO not clear all errors will result in total cleanup
369 // File configurationDir = configurationData.getConfigurationDir();
370 // if (!DeploymentUtil.recursiveDelete(configurationDir)) {
371 // pendingDeletionIndex.setProperty(configurationDir.getName(), new String("delete"));
372 // log.debug("Queued deployment directory to be reaped " + configurationDir);
373 // }
374 // if (targetFile != null) {
375 // targetFile.delete();
376 // }
377
378 if (e instanceof Error) {
379 log.error("Deployment failed due to ", e);
380 throw (Error) e;
381 } else if (e instanceof DeploymentException) {
382 throw (DeploymentException) e;
383 } else if (e instanceof Exception) {
384 log.error("Deployment failed due to ", e);
385 throw new DeploymentException(e);
386 }
387 throw new Error(e);
388 } finally {
389 DeploymentUtil.close(module);
390 }
391 }
392
393 private void notifyWatchers(List list) {
394 Artifact[] arts = new Artifact[list.size()];
395 for (int i = 0; i < list.size(); i++) {
396 String s = (String) list.get(i);
397 arts[i] = Artifact.create(s);
398 }
399 for (Iterator it = watchers.iterator(); it.hasNext();) {
400 DeploymentWatcher watcher = (DeploymentWatcher) it.next();
401 for (int i = 0; i < arts.length; i++) {
402 Artifact art = arts[i];
403 watcher.deployed(art);
404 }
405 }
406 }
407
408 private void cleanupConfigurations(List configurations) {
409 for (Iterator iterator = configurations.iterator(); iterator.hasNext();) {
410 ConfigurationData configurationData = (ConfigurationData) iterator.next();
411 File configurationDir = configurationData.getConfigurationDir();
412 if (!DeploymentUtil.recursiveDelete(configurationDir)) {
413 pendingDeletionIndex.setProperty(configurationDir.getName(), "delete");
414 log.debug("Queued deployment directory to be reaped " + configurationDir);
415 }
416 }
417 }
418
419 /**
420 * Thread to cleanup unused temporary Deployer directories (and files).
421 * On Windows, open files can't be deleted. Until MultiParentClassLoaders
422 * are GC'ed, we won't be able to delete Config Store directories/files.
423 */
424 class DeployerReaper implements Runnable {
425 private final int reaperInterval;
426 private volatile boolean done = false;
427
428 public DeployerReaper(int reaperInterval) {
429 this.reaperInterval = reaperInterval;
430 }
431
432 public void close() {
433 this.done = true;
434 }
435
436 public void run() {
437 log.debug("ConfigStoreReaper started");
438 while (!done) {
439 try {
440 Thread.sleep(reaperInterval);
441 } catch (InterruptedException e) {
442 continue;
443 }
444 reap();
445 }
446 }
447
448 /**
449 * For every directory in the pendingDeletionIndex, attempt to delete all
450 * sub-directories and files.
451 */
452 public void reap() {
453 // return, if there's nothing to do
454 if (pendingDeletionIndex.size() == 0)
455 return;
456 // Otherwise, attempt to delete all of the directories
457 Enumeration list = pendingDeletionIndex.propertyNames();
458 while (list.hasMoreElements()) {
459 String dirName = (String) list.nextElement();
460 File deleteDir = new File(dirName);
461
462 if (!DeploymentUtil.recursiveDelete(deleteDir)) {
463 pendingDeletionIndex.remove(deleteDir);
464 log.debug("Reaped deployment directory " + deleteDir);
465 }
466 }
467 }
468 }
469
470 public static final GBeanInfo GBEAN_INFO;
471
472 private static final String DEPLOYER = "Deployer";
473
474 static {
475 GBeanInfoBuilder infoFactory = GBeanInfoBuilder.createStatic(Deployer.class, DEPLOYER);
476
477 infoFactory.addAttribute("kernel", Kernel.class, false);
478 infoFactory.addAttribute("remoteDeployAddress", String.class, true, true);
479 infoFactory.addAttribute("remoteDeployUploadURL", String.class, false);
480 infoFactory.addOperation("deploy", new Class[]{boolean.class, File.class, File.class});
481 infoFactory.addOperation("deploy", new Class[]{boolean.class, File.class, File.class, String.class});
482 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});
483
484 infoFactory.addReference("Builders", ConfigurationBuilder.class, "ConfigBuilder");
485 infoFactory.addReference("Store", ConfigurationStore.class, "ConfigurationStore");
486 infoFactory.addReference("Watchers", DeploymentWatcher.class);
487
488 infoFactory.setConstructor(new String[]{"remoteDeployAddress", "Builders", "Store", "Watchers", "kernel"});
489
490 GBEAN_INFO = infoFactory.getBeanInfo();
491 }
492
493 public static GBeanInfo getGBeanInfo() {
494 return GBEAN_INFO;
495 }
496 }