/* * JBoss, Home of Professional Open Source. * Copyright 2010, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.wildfly.core.embedded; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.StandardCopyOption; import java.security.SecureRandom; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.jboss.as.controller.ControlledProcessState; import org.jboss.as.controller.ControlledProcessStateService; import org.jboss.as.controller.ModelController; import org.jboss.as.controller.ProcessType; import org.jboss.as.controller.client.ModelControllerClient; import org.jboss.as.controller.client.helpers.DelegatingModelControllerClient; import org.jboss.as.host.controller.DomainModelControllerService; import org.jboss.as.host.controller.HostControllerEnvironment; import org.jboss.as.host.controller.Main; import org.jboss.as.process.ProcessController; import org.jboss.as.server.FutureServiceContainer; import org.jboss.as.server.SystemExiter; import org.jboss.as.server.logging.ServerLogger; import org.jboss.modules.ModuleLoader; import org.jboss.msc.service.ServiceContainer; import org.jboss.msc.service.ServiceController; import org.jboss.msc.value.Value; import org.jboss.stdio.StdioContext; import org.wildfly.common.Assert; import org.wildfly.core.embedded.logging.EmbeddedLogger; import org.wildfly.security.manager.WildFlySecurityManager; /** * This is the host controller counterpart to EmbeddedServerFactory which lives behind a module class loader. * <p> * HostContollerFactory that sets up an embedded server using modular classloading. * </p> * <p> * To use this class the <code>jboss.home.dir</code> system property must be set to the * application server home directory. By default it will use the directories * <code>{$jboss.home.dir}/standalone/config</code> as the <i>configuration</i> directory and * <code>{$jboss.home.dir}/standalone/data</code> as the <i>data</i> directory. This can be overridden * with the <code>${jboss.server.base.dir}</code>, <code>${jboss.server.config.dir}</code> or <code>${jboss.server.config.dir}</code> * system properties as for normal server startup. * </p> * <p> * If a clean run is wanted, you can specify <code>${jboss.embedded.root}</code> to an existing directory * which will copy the contents of the data and configuration directories under a temporary folder. This * has the effect of this run not polluting later runs of the embedded server. * </p> * * @author Ken Wills <kwills@redhat.com> * @see EmbeddedProcessFactory */ public class EmbeddedHostControllerFactory { public static final String JBOSS_EMBEDDED_ROOT = "jboss.embedded.root"; private static final String MODULE_PATH = "-mp"; private static final String PC_ADDRESS = "--pc-address"; private static final String PC_PORT = "--pc-port"; private EmbeddedHostControllerFactory() { } public static HostController create(final File jbossHomeDir, final ModuleLoader moduleLoader, final Properties systemProps, final Map<String, String> systemEnv, final String[] cmdargs) { Assert.checkNotNullParam("jbossHomeDir", jbossHomeDir); Assert.checkNotNullParam("moduleLoader", moduleLoader); Assert.checkNotNullParam("systemProps", systemProps); Assert.checkNotNullParam("systemEnv", systemEnv); Assert.checkNotNullParam("cmdargs", cmdargs); setupCleanDirectories(jbossHomeDir, systemProps); return new HostControllerImpl(jbossHomeDir, cmdargs, systemProps, systemEnv, moduleLoader); } static void setupCleanDirectories(File jbossHomeDir, Properties props) { File tempRoot = getTempRoot(props); if (tempRoot == null) { return; } File originalConfigDir = getFileUnderAsRoot(jbossHomeDir, props, HostControllerEnvironment.DOMAIN_CONFIG_DIR, "configuration", true); File originalDataDir = getFileUnderAsRoot(jbossHomeDir, props, HostControllerEnvironment.DOMAIN_DATA_DIR, "data", false); try { File configDir = new File(tempRoot, "config"); Files.createDirectory(configDir.toPath()); File dataDir = new File(tempRoot, "data"); Files.createDirectory(dataDir.toPath()); // For jboss.server.deployment.scanner.default File deploymentsDir = new File(tempRoot, "deployments"); Files.createDirectory(deploymentsDir.toPath()); copyDirectory(originalConfigDir, configDir); if (originalDataDir.exists()) { copyDirectory(originalDataDir, dataDir); } props.put(HostControllerEnvironment.DOMAIN_BASE_DIR, tempRoot.getAbsolutePath()); props.put(HostControllerEnvironment.DOMAIN_CONFIG_DIR, configDir.getAbsolutePath()); props.put(HostControllerEnvironment.DOMAIN_DATA_DIR, dataDir.getAbsolutePath()); } catch (IOException e) { throw EmbeddedLogger.ROOT_LOGGER.cannotSetupEmbeddedServer(e); } } private static File getFileUnderAsRoot(File jbossHomeDir, Properties props, String propName, String relativeLocation, boolean mustExist) { String prop = props.getProperty(propName, null); if (prop == null) { prop = props.getProperty(HostControllerEnvironment.DOMAIN_BASE_DIR, null); if (prop == null) { File dir = new File(jbossHomeDir, "domain" + File.separator + relativeLocation); if (mustExist && (!dir.exists() || !dir.isDirectory())) { throw ServerLogger.ROOT_LOGGER.embeddedServerDirectoryNotFound("domain" + File.separator + relativeLocation, jbossHomeDir.getAbsolutePath()); } return dir; } else { File server = new File(prop); validateDirectory(HostControllerEnvironment.DOMAIN_BASE_DIR, server); return new File(server, relativeLocation); } } else { File dir = new File(prop); validateDirectory(HostControllerEnvironment.DOMAIN_BASE_DIR, dir); return dir; } } private static File getTempRoot(Properties props) { String tempRoot = props.getProperty(JBOSS_EMBEDDED_ROOT, null); if (tempRoot == null) { return null; } try { File root = new File(tempRoot); if (!root.exists()) { //Attempt to try to create the directory, in case something like target/embedded was specified Files.createDirectories(root.toPath()); } validateDirectory("jboss.test.clean.root", root); root = new File(root, "configs"); Files.createDirectories(root.toPath()); SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmssSSS"); root = new File(root, format.format(new Date())); Files.createDirectory(root.toPath()); return root; } catch (IOException e) { throw EmbeddedLogger.ROOT_LOGGER.cannotSetupEmbeddedServer(e); } } private static void validateDirectory(String property, File file) { if (!file.exists()) { throw ServerLogger.ROOT_LOGGER.propertySpecifiedFileDoesNotExist(property, file.getAbsolutePath()); } if (!file.isDirectory()) { throw ServerLogger.ROOT_LOGGER.propertySpecifiedFileIsNotADirectory(property, file.getAbsolutePath()); } } private static void copyDirectory(File src, File dest) { if(src.list() != null) { for (String current : src.list()) { final File srcFile = new File(src, current); final File destFile = new File(dest, current); try { Files.copy(srcFile.toPath(), destFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); if (srcFile.isDirectory()) { copyDirectory(srcFile, destFile); } } catch (IOException e) { throw ServerLogger.ROOT_LOGGER.errorCopyingFile(srcFile.getAbsolutePath(), destFile.getAbsolutePath(), e); } } } } private static class HostControllerImpl implements HostController { private final PropertyChangeListener processStateListener; private final String[] cmdargs; private final File jbossHomeDir; private final Properties systemProps; private final Map<String, String> systemEnv; private final ModuleLoader moduleLoader; private ServiceContainer serviceContainer; private ControlledProcessState.State currentProcessState; private ModelControllerClient modelControllerClient; private ExecutorService executorService; private ControlledProcessStateService controlledProcessStateService; private boolean uninstallStdIo; public HostControllerImpl(final File jbossHomeDir, String[] cmdargs, Properties systemProps, Map<String, String> systemEnv, ModuleLoader moduleLoader) { this.cmdargs = cmdargs; this.jbossHomeDir = jbossHomeDir; this.systemProps = systemProps; this.systemEnv = systemEnv; this.moduleLoader = moduleLoader; processStateListener = new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { if ("currentState".equals(evt.getPropertyName())) { ControlledProcessState.State newState = (ControlledProcessState.State) evt.getNewValue(); establishModelControllerClient(newState); } } }; } @Override public void start() throws EmbeddedProcessStartException { EmbeddedHostControllerBootstrap hostControllerBootstrap = null; try { final long startTime = System.currentTimeMillis(); // Take control of server use of System.exit SystemExiter.initialize(new SystemExiter.Exiter() { @Override public void exit(int status) { HostControllerImpl.this.exit(); } }); // Take control of stdio try { StdioContext.install(); uninstallStdIo = true; } catch (IllegalStateException ignored) { // already installed } // Determine the ServerEnvironment HostControllerEnvironment environment = createHostControllerEnvironment(jbossHomeDir, cmdargs, startTime); FutureServiceContainer futureContainer = new FutureServiceContainer(); final byte[] authBytes = new byte[ProcessController.AUTH_BYTES_LENGTH]; new Random(new SecureRandom().nextLong()).nextBytes(authBytes); final String authCode = Base64.getEncoder().encodeToString(authBytes); hostControllerBootstrap = new EmbeddedHostControllerBootstrap(futureContainer, environment, authCode); hostControllerBootstrap.bootstrap(); serviceContainer = futureContainer.get(); executorService = Executors.newCachedThreadPool(); @SuppressWarnings("unchecked") final Value<ControlledProcessStateService> processStateServiceValue = (Value<ControlledProcessStateService>) serviceContainer.getRequiredService(ControlledProcessStateService.SERVICE_NAME); controlledProcessStateService = processStateServiceValue.getValue(); controlledProcessStateService.addPropertyChangeListener(processStateListener); establishModelControllerClient(controlledProcessStateService.getCurrentState()); } catch (RuntimeException rte) { if (hostControllerBootstrap != null) { hostControllerBootstrap.failed(); } throw rte; } catch (Exception ex) { if (hostControllerBootstrap != null) { hostControllerBootstrap.failed(); } throw EmbeddedLogger.ROOT_LOGGER.cannotStartEmbeddedServer(ex); } } @Override public synchronized ModelControllerClient getModelControllerClient() { return modelControllerClient == null ? null : new DelegatingModelControllerClient(new DelegatingModelControllerClient.DelegateProvider() { @Override public ModelControllerClient getDelegate() { return getActiveModelControllerClient(); } }); } private synchronized void establishModelControllerClient(ControlledProcessState.State state) { ModelControllerClient newClient = null; if (state != ControlledProcessState.State.STOPPING && state != ControlledProcessState.State.STOPPED && serviceContainer != null) { @SuppressWarnings("unchecked") final ServiceController<ModelController> modelControllerValue = (ServiceController<ModelController>) serviceContainer.getService(DomainModelControllerService.SERVICE_NAME); if (modelControllerValue != null) { final ModelController controller = modelControllerValue.getValue(); newClient = controller.createClient(executorService); } } modelControllerClient = newClient; currentProcessState = state; } private synchronized ModelControllerClient getActiveModelControllerClient() { switch (currentProcessState) { case STOPPING: { throw EmbeddedLogger.ROOT_LOGGER.processIsStopping(); } case STOPPED: { throw EmbeddedLogger.ROOT_LOGGER.processIsStopped(); } case STARTING: { if (modelControllerClient == null) { // Service wasn't available when we got the ControlledProcessState // state change notification; try again establishModelControllerClient(currentProcessState); if (modelControllerClient == null) { throw EmbeddedLogger.ROOT_LOGGER.processIsReloading(); } } // fall through } default: { return modelControllerClient; } } } @Override public void stop() { exit(); } private void exit() { if (serviceContainer != null) { try { serviceContainer.shutdown(); serviceContainer.awaitTermination(); } catch (RuntimeException rte) { throw rte; } catch (InterruptedException ite) { ite.printStackTrace(); Thread.currentThread().interrupt(); } catch (Exception ex) { ex.printStackTrace(); } } if (controlledProcessStateService != null) { controlledProcessStateService.removePropertyChangeListener(processStateListener); controlledProcessStateService = null; } if (executorService != null) { try { executorService.shutdown(); // 10 secs is arbitrary, but if the service container is terminated, // no good can happen from waiting for ModelControllerClient requests to complete executorService.awaitTermination(10, TimeUnit.SECONDS); } catch (RuntimeException rte) { throw rte; } catch (InterruptedException ite) { ite.printStackTrace(); Thread.currentThread().interrupt(); } catch (Exception ex) { ex.printStackTrace(); } } if (uninstallStdIo) { try { StdioContext.uninstall(); } catch (IllegalStateException ignored) { // something else already did } } SystemExiter.initialize(SystemExiter.Exiter.DEFAULT); } private static HostControllerEnvironment createHostControllerEnvironment(File jbossHome, String[] cmdargs, long startTime) { WildFlySecurityManager.setPropertyPrivileged(HostControllerEnvironment.HOME_DIR, jbossHome.getAbsolutePath()); List<String> cmds = new ArrayList<String>(Arrays.asList(cmdargs)); // these are for compatibility with Main.determineEnvironment / HostControllerEnvironment // Once WFCORE-938 is resolved, --admin-only will allow a connection back to the DC for slaves, // and support a method for setting the domain master address outside of -Djboss.domain.master.address // so we'll probably need a command line argument for this if its not specified as a system prop if (WildFlySecurityManager.getPropertyPrivileged(HostControllerEnvironment.JBOSS_DOMAIN_MASTER_ADDRESS, null) == null) { WildFlySecurityManager.setPropertyPrivileged(HostControllerEnvironment.JBOSS_DOMAIN_MASTER_ADDRESS, "127.0.0.1"); } cmds.add(MODULE_PATH); cmds.add(WildFlySecurityManager.getPropertyPrivileged("module.path", "")); cmds.add(PC_ADDRESS); cmds.add("0"); cmds.add(PC_PORT); cmds.add("0"); // this used to be set in the embedded-hc specific env setup, WFCORE-938 will add support for --admin-only=false cmds.add("--admin-only"); for (final String prop : EmbeddedProcessFactory.DOMAIN_KEYS) { // if we've started with any jboss.domain.base.dir etc, copy those in here. String value = WildFlySecurityManager.getPropertyPrivileged(prop, null); if (value != null) cmds.add("-D" + prop + "=" + value); } return Main.determineEnvironment(cmds.toArray(new String[cmds.size()]), startTime, ProcessType.EMBEDDED_HOST_CONTROLLER).getHostControllerEnvironment(); } } }