mirror of
https://github.com/scm-manager/scm-manager.git
synced 2025-11-13 17:05:43 +01:00
rename package sonia.scm.boot to sonia.scm.lifecycle
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.inject.Module;
|
||||
import org.apache.shiro.guice.web.ShiroWebModule;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ClassOverrides;
|
||||
import sonia.scm.ResteasyModule;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.ScmSecurityModule;
|
||||
import sonia.scm.ScmServletModule;
|
||||
import sonia.scm.Stage;
|
||||
import sonia.scm.api.v2.resources.MapperModule;
|
||||
import sonia.scm.debug.DebugModule;
|
||||
import sonia.scm.filter.WebElementModule;
|
||||
import sonia.scm.plugin.ExtensionProcessor;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class ApplicationModuleProvider implements ModuleProvider {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ApplicationModuleProvider.class);
|
||||
|
||||
private final ServletContext servletContext;
|
||||
private final PluginLoader pluginLoader;
|
||||
|
||||
ApplicationModuleProvider(ServletContext servletContext, PluginLoader pluginLoader) {
|
||||
this.servletContext = servletContext;
|
||||
this.pluginLoader = pluginLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Module> createModules() {
|
||||
ClassOverrides overrides = createClassOverrides();
|
||||
return createModules(overrides);
|
||||
}
|
||||
|
||||
private List<Module> createModules(ClassOverrides overrides) {
|
||||
List<Module> moduleList = new ArrayList<>();
|
||||
moduleList.add(new ResteasyModule());
|
||||
moduleList.add(ShiroWebModule.guiceFilterModule());
|
||||
moduleList.add(new WebElementModule(pluginLoader));
|
||||
moduleList.add(new ScmServletModule(pluginLoader, overrides));
|
||||
moduleList.add(
|
||||
new ScmSecurityModule(servletContext, pluginLoader.getExtensionProcessor())
|
||||
);
|
||||
appendModules(pluginLoader.getExtensionProcessor(), moduleList);
|
||||
moduleList.addAll(overrides.getModules());
|
||||
|
||||
if (SCMContext.getContext().getStage() == Stage.DEVELOPMENT){
|
||||
moduleList.add(new DebugModule());
|
||||
}
|
||||
moduleList.add(new MapperModule());
|
||||
|
||||
return moduleList;
|
||||
}
|
||||
|
||||
private ClassOverrides createClassOverrides() {
|
||||
ClassLoader uberClassLoader = pluginLoader.getUberClassLoader();
|
||||
return ClassOverrides.findOverrides(uberClassLoader);
|
||||
}
|
||||
|
||||
private void appendModules(ExtensionProcessor ep, List<Module> moduleList) {
|
||||
for (Class<? extends Module> module : ep.byExtensionPoint(Module.class)) {
|
||||
try {
|
||||
LOG.info("add module {}", module);
|
||||
moduleList.add(module.newInstance());
|
||||
} catch (IllegalAccessException | InstantiationException ex) {
|
||||
throw Throwables.propagate(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
/**
|
||||
* This ClassLoader is mainly a wrapper around the web application class loader and its goal is to make it easier to
|
||||
* find it in a heap dump.
|
||||
*/
|
||||
class BootstrapClassLoader extends ClassLoader {
|
||||
BootstrapClassLoader(ClassLoader webappClassLoader) {
|
||||
super(webappClassLoader);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Copyright (c) 2014, Sebastian Sdorra All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer. 2. Redistributions in
|
||||
* binary form must reproduce the above copyright notice, this list of
|
||||
* conditions and the following disclaimer in the documentation and/or other
|
||||
* materials provided with the distribution. 3. Neither the name of SCM-Manager;
|
||||
* nor the names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
//~--- non-JDK imports --------------------------------------------------------
|
||||
|
||||
import com.github.legman.Subscribe;
|
||||
import com.google.inject.servlet.GuiceFilter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
|
||||
import javax.servlet.FilterConfig;
|
||||
import javax.servlet.ServletContextEvent;
|
||||
import javax.servlet.ServletException;
|
||||
|
||||
//~--- JDK imports ------------------------------------------------------------
|
||||
|
||||
/**
|
||||
*
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class BootstrapContextFilter extends GuiceFilter {
|
||||
|
||||
/**
|
||||
* the logger for BootstrapContextFilter
|
||||
*/
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BootstrapContextFilter.class);
|
||||
|
||||
private final BootstrapContextListener listener = new BootstrapContextListener();
|
||||
|
||||
/** Field description */
|
||||
private FilterConfig filterConfig;
|
||||
|
||||
@Override
|
||||
public void init(FilterConfig filterConfig) throws ServletException {
|
||||
this.filterConfig = filterConfig;
|
||||
|
||||
initializeContext();
|
||||
}
|
||||
|
||||
private void initializeContext() throws ServletException {
|
||||
super.init(filterConfig);
|
||||
|
||||
LOG.info("register for restart events");
|
||||
ScmEventBus.getInstance().register(this);
|
||||
|
||||
listener.contextInitialized(new ServletContextEvent(filterConfig.getServletContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
super.destroy();
|
||||
|
||||
listener.contextDestroyed(new ServletContextEvent(filterConfig.getServletContext()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart SCM-Manager.
|
||||
*
|
||||
* @param event restart event
|
||||
*/
|
||||
@Subscribe
|
||||
public void handleRestartEvent(RestartEvent event) {
|
||||
LOG.warn("received restart event from {} with reason: {}",
|
||||
event.getCause(), event.getReason());
|
||||
|
||||
if (filterConfig == null) {
|
||||
LOG.error("filter config is null, scm-manager is not initialized");
|
||||
} else {
|
||||
RestartStrategy restartStrategy = RestartStrategy.get();
|
||||
restartStrategy.restart(new GuiceInjectionContext());
|
||||
}
|
||||
}
|
||||
|
||||
private class GuiceInjectionContext implements RestartStrategy.InjectionContext {
|
||||
|
||||
@Override
|
||||
public void initialize() {
|
||||
try {
|
||||
BootstrapContextFilter.this.initializeContext();
|
||||
} catch (ServletException e) {
|
||||
throw new IllegalStateException("failed to initialize guice", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() {
|
||||
BootstrapContextFilter.this.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* Copyright (c) 2010, Sebastian Sdorra All rights reserved.
|
||||
* <p>
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
* <p>
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer. 2. Redistributions in
|
||||
* binary form must reproduce the above copyright notice, this list of
|
||||
* conditions and the following disclaimer in the documentation and/or other
|
||||
* materials provided with the distribution. 3. Neither the name of SCM-Manager;
|
||||
* nor the names of its contributors may be used to endorse or promote products
|
||||
* derived from this software without specific prior written permission.
|
||||
* <p>
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
* ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
* <p>
|
||||
* http://bitbucket.org/sdorra/scm-manager
|
||||
*/
|
||||
|
||||
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.inject.Guice;
|
||||
import com.google.inject.Injector;
|
||||
import com.google.inject.Module;
|
||||
import com.google.inject.servlet.GuiceServletContextListener;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.CloseableModule;
|
||||
import sonia.scm.EagerSingletonModule;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.ScmEventBusModule;
|
||||
import sonia.scm.ScmInitializerModule;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.update.MigrationWizardModuleProvider;
|
||||
import sonia.scm.update.UpdateEngine;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletContextEvent;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author Sebastian Sdorra
|
||||
*/
|
||||
public class BootstrapContextListener extends GuiceServletContextListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BootstrapContextListener.class);
|
||||
|
||||
private final ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create();
|
||||
|
||||
private ServletContext context;
|
||||
private InjectionLifeCycle injectionLifeCycle;
|
||||
|
||||
@Override
|
||||
public void contextInitialized(ServletContextEvent sce) {
|
||||
LOG.info("start scm-manager initialization");
|
||||
|
||||
context = sce.getServletContext();
|
||||
classLoaderLifeCycle.init();
|
||||
super.contextInitialized(sce);
|
||||
|
||||
Injector injector = (Injector) context.getAttribute(Injector.class.getName());
|
||||
injectionLifeCycle = new InjectionLifeCycle(injector);
|
||||
injectionLifeCycle.initialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Injector getInjector() {
|
||||
Throwable startupError = SCMContext.getContext().getStartupError();
|
||||
if (startupError != null) {
|
||||
return createStageOneInjector(SingleView.error(startupError));
|
||||
} else if (Versions.isTooOld()) {
|
||||
return createStageOneInjector(SingleView.view("/templates/too-old.mustache", HttpServletResponse.SC_CONFLICT));
|
||||
} else {
|
||||
try {
|
||||
return createStageTwoInjector();
|
||||
} catch (Exception ex) {
|
||||
return createStageOneInjector(SingleView.error(ex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextDestroyed(ServletContextEvent sce) {
|
||||
LOG.info("shutdown scm-manager context");
|
||||
|
||||
ServletContextCleaner.cleanup(context);
|
||||
|
||||
injectionLifeCycle.shutdown();
|
||||
injectionLifeCycle = null;
|
||||
classLoaderLifeCycle.shutdown();
|
||||
}
|
||||
|
||||
private Injector createStageTwoInjector() {
|
||||
PluginBootstrap pluginBootstrap = new PluginBootstrap(context, classLoaderLifeCycle);
|
||||
|
||||
ModuleProvider provider = createMigrationOrNormalModuleProvider(pluginBootstrap);
|
||||
return createStageTwoInjector(provider, pluginBootstrap.getPluginLoader());
|
||||
}
|
||||
|
||||
private ModuleProvider createMigrationOrNormalModuleProvider(PluginBootstrap pluginBootstrap) {
|
||||
Injector bootstrapInjector = createBootstrapInjector(pluginBootstrap.getPluginLoader());
|
||||
|
||||
return startEitherMigrationOrApplication(pluginBootstrap.getPluginLoader(), bootstrapInjector);
|
||||
}
|
||||
|
||||
private ModuleProvider startEitherMigrationOrApplication(PluginLoader pluginLoader, Injector bootstrapInjector) {
|
||||
MigrationWizardModuleProvider wizardModuleProvider = new MigrationWizardModuleProvider(bootstrapInjector);
|
||||
|
||||
if (wizardModuleProvider.wizardNecessary()) {
|
||||
return wizardModuleProvider;
|
||||
} else {
|
||||
processUpdates(pluginLoader, bootstrapInjector);
|
||||
|
||||
Versions.writeNew();
|
||||
|
||||
return new ApplicationModuleProvider(context, pluginLoader);
|
||||
}
|
||||
}
|
||||
|
||||
private Injector createStageOneInjector(ModuleProvider provider) {
|
||||
return Guice.createInjector(provider.createModules());
|
||||
}
|
||||
|
||||
private Injector createStageTwoInjector(ModuleProvider provider, PluginLoader pluginLoader) {
|
||||
List<Module> modules = new ArrayList<>(createBootstrapModules(pluginLoader));
|
||||
modules.addAll(provider.createModules());
|
||||
return Guice.createInjector(modules);
|
||||
}
|
||||
|
||||
private Injector createBootstrapInjector(PluginLoader pluginLoader) {
|
||||
return Guice.createInjector(createBootstrapModules(pluginLoader));
|
||||
}
|
||||
|
||||
private List<Module> createBootstrapModules(PluginLoader pluginLoader) {
|
||||
List<Module> modules = new ArrayList<>(createBaseModules());
|
||||
modules.add(new BootstrapModule(pluginLoader));
|
||||
return modules;
|
||||
}
|
||||
|
||||
private List<Module> createBaseModules() {
|
||||
return ImmutableList.of(
|
||||
new EagerSingletonModule(),
|
||||
new ScmInitializerModule(),
|
||||
new ScmEventBusModule(),
|
||||
new ServletContextModule(),
|
||||
new CloseableModule()
|
||||
);
|
||||
}
|
||||
|
||||
private void processUpdates(PluginLoader pluginLoader, Injector bootstrapInjector) {
|
||||
Injector updateInjector = bootstrapInjector.createChildInjector(new UpdateStepModule(pluginLoader));
|
||||
|
||||
UpdateEngine updateEngine = updateInjector.getInstance(UpdateEngine.class);
|
||||
updateEngine.update();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.throwingproviders.ThrowingProviderBinder;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.ClassOverrides;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.io.DefaultFileSystem;
|
||||
import sonia.scm.io.FileSystem;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.repository.RepositoryLocationResolver;
|
||||
import sonia.scm.repository.xml.PathBasedRepositoryLocationResolver;
|
||||
import sonia.scm.security.CipherHandler;
|
||||
import sonia.scm.security.CipherUtil;
|
||||
import sonia.scm.security.DefaultKeyGenerator;
|
||||
import sonia.scm.security.KeyGenerator;
|
||||
import sonia.scm.store.BlobStoreFactory;
|
||||
import sonia.scm.store.ConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.ConfigurationStoreFactory;
|
||||
import sonia.scm.store.DataStoreFactory;
|
||||
import sonia.scm.store.FileBlobStoreFactory;
|
||||
import sonia.scm.store.JAXBConfigurationEntryStoreFactory;
|
||||
import sonia.scm.store.JAXBConfigurationStoreFactory;
|
||||
import sonia.scm.store.JAXBDataStoreFactory;
|
||||
|
||||
public class BootstrapModule extends AbstractModule {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(BootstrapModule.class);
|
||||
|
||||
private final ClassOverrides overrides;
|
||||
private final PluginLoader pluginLoader;
|
||||
|
||||
BootstrapModule(PluginLoader pluginLoader) {
|
||||
this.overrides = ClassOverrides.findOverrides(pluginLoader.getUberClassLoader());
|
||||
this.pluginLoader = pluginLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
install(ThrowingProviderBinder.forModule(this));
|
||||
|
||||
SCMContextProvider context = SCMContext.getContext();
|
||||
|
||||
bind(SCMContextProvider.class).toInstance(context);
|
||||
|
||||
bind(KeyGenerator.class).to(DefaultKeyGenerator.class);
|
||||
|
||||
bind(RepositoryLocationResolver.class).to(PathBasedRepositoryLocationResolver.class);
|
||||
|
||||
bind(FileSystem.class, DefaultFileSystem.class);
|
||||
|
||||
// note CipherUtil uses an other generator
|
||||
bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler());
|
||||
|
||||
// bind core
|
||||
bind(ConfigurationStoreFactory.class, JAXBConfigurationStoreFactory.class);
|
||||
bind(ConfigurationEntryStoreFactory.class, JAXBConfigurationEntryStoreFactory.class);
|
||||
bind(DataStoreFactory.class, JAXBDataStoreFactory.class);
|
||||
bind(BlobStoreFactory.class, FileBlobStoreFactory.class);
|
||||
bind(PluginLoader.class).toInstance(pluginLoader);
|
||||
}
|
||||
|
||||
private <T> void bind(Class<T> clazz, Class<? extends T> defaultImplementation) {
|
||||
Class<? extends T> implementation = find(clazz, defaultImplementation);
|
||||
LOG.debug("bind {} to {}", clazz, implementation);
|
||||
bind(clazz).to(implementation);
|
||||
}
|
||||
|
||||
private <T> Class<? extends T> find(Class<T> clazz, Class<? extends T> defaultImplementation) {
|
||||
Class<? extends T> implementation = overrides.getOverride(clazz);
|
||||
|
||||
if (implementation != null) {
|
||||
LOG.info("found override {} for {}", implementation, clazz);
|
||||
} else {
|
||||
implementation = defaultImplementation;
|
||||
|
||||
LOG.trace(
|
||||
"no override available for {}, using default implementation {}",
|
||||
clazz, implementation);
|
||||
}
|
||||
|
||||
return implementation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
|
||||
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventorFactory;
|
||||
import se.jiderhamn.classloader.leak.prevention.cleanup.MBeanCleanUp;
|
||||
import sonia.scm.plugin.ChildFirstPluginClassLoader;
|
||||
import sonia.scm.plugin.DefaultPluginClassLoader;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkState;
|
||||
|
||||
/**
|
||||
* Creates and shutdown SCM-Manager ClassLoaders.
|
||||
*/
|
||||
public final class ClassLoaderLifeCycle {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class);
|
||||
|
||||
private final Deque<ClassLoaderAndPreventor> classLoaders = new ArrayDeque<>();
|
||||
|
||||
private final ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory;
|
||||
private final ClassLoader webappClassLoader;
|
||||
|
||||
private ClassLoader bootstrapClassLoader;
|
||||
private UnaryOperator<ClassLoader> classLoaderAppendListener = c -> c;
|
||||
|
||||
@VisibleForTesting
|
||||
public static ClassLoaderLifeCycle create() {
|
||||
ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory = new ClassLoaderLeakPreventorFactory();
|
||||
classLoaderLeakPreventorFactory.setLogger(new LoggingAdapter());
|
||||
classLoaderLeakPreventorFactory.removeCleanUp(MBeanCleanUp.class);
|
||||
return new ClassLoaderLifeCycle(Thread.currentThread().getContextClassLoader(), classLoaderLeakPreventorFactory);
|
||||
}
|
||||
|
||||
ClassLoaderLifeCycle(ClassLoader webappClassLoader, ClassLoaderLeakPreventorFactory classLoaderLeakPreventorFactory) {
|
||||
this.classLoaderLeakPreventorFactory = classLoaderLeakPreventorFactory;
|
||||
this.webappClassLoader = initAndAppend(webappClassLoader);
|
||||
}
|
||||
|
||||
void init() {
|
||||
bootstrapClassLoader = initAndAppend(new BootstrapClassLoader(webappClassLoader));
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void setClassLoaderAppendListener(UnaryOperator<ClassLoader> classLoaderAppendListener) {
|
||||
this.classLoaderAppendListener = classLoaderAppendListener;
|
||||
}
|
||||
|
||||
public ClassLoader getBootstrapClassLoader() {
|
||||
checkState(bootstrapClassLoader != null, "%s was not initialized", ClassLoaderLifeCycle.class.getName());
|
||||
return bootstrapClassLoader;
|
||||
}
|
||||
|
||||
public ClassLoader createPluginClassLoader(URL[] urls, ClassLoader parent, String plugin) {
|
||||
LOG.debug("create new PluginClassLoader for {}", plugin);
|
||||
DefaultPluginClassLoader pluginClassLoader = new DefaultPluginClassLoader(urls, parent, plugin);
|
||||
return initAndAppend(pluginClassLoader);
|
||||
}
|
||||
|
||||
public ClassLoader createChildFirstPluginClassLoader(URL[] urls, ClassLoader parent, String plugin) {
|
||||
LOG.debug("create new ChildFirstPluginClassLoader for {}", plugin);
|
||||
ChildFirstPluginClassLoader pluginClassLoader = new ChildFirstPluginClassLoader(urls, parent, plugin);
|
||||
return initAndAppend(pluginClassLoader);
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
LOG.info("shutdown classloader infrastructure");
|
||||
ClassLoaderAndPreventor clap = classLoaders.poll();
|
||||
while (clap != null) {
|
||||
clap.shutdown();
|
||||
clap = classLoaders.poll();
|
||||
}
|
||||
bootstrapClassLoader = null;
|
||||
}
|
||||
|
||||
private ClassLoader initAndAppend(ClassLoader originalClassLoader) {
|
||||
LOG.debug("init classloader {}", originalClassLoader);
|
||||
ClassLoader classLoader = classLoaderAppendListener.apply(originalClassLoader);
|
||||
|
||||
ClassLoaderLeakPreventor preventor = classLoaderLeakPreventorFactory.newLeakPreventor(classLoader);
|
||||
preventor.runPreClassLoaderInitiators();
|
||||
classLoaders.push(new ClassLoaderAndPreventor(classLoader, preventor));
|
||||
|
||||
return classLoader;
|
||||
}
|
||||
|
||||
private class ClassLoaderAndPreventor {
|
||||
|
||||
private final ClassLoader classLoader;
|
||||
private final ClassLoaderLeakPreventor preventor;
|
||||
|
||||
private ClassLoaderAndPreventor(ClassLoader classLoader, ClassLoaderLeakPreventor preventor) {
|
||||
this.classLoader = classLoader;
|
||||
this.preventor = preventor;
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
LOG.debug("shutdown classloader {}", classLoader);
|
||||
preventor.runCleanUps();
|
||||
|
||||
if (classLoader != webappClassLoader) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
private void close() {
|
||||
if (classLoader instanceof Closeable) {
|
||||
LOG.trace("close classloader {}", classLoader);
|
||||
try {
|
||||
((Closeable) classLoader).close();
|
||||
} catch (IOException e) {
|
||||
LOG.warn("failed to close classloader", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.event.RecreateEventBusEvent;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* Restart strategy implementation which destroy the injection context and re initialize it.
|
||||
*/
|
||||
public class InjectionContextRestartStrategy implements RestartStrategy {
|
||||
|
||||
private static final AtomicLong INSTANCE_COUNTER = new AtomicLong();
|
||||
private static final Logger LOG = LoggerFactory.getLogger(InjectionContextRestartStrategy.class);
|
||||
|
||||
private long waitInMs = 250L;
|
||||
|
||||
@VisibleForTesting
|
||||
void setWaitInMs(long waitInMs) {
|
||||
this.waitInMs = waitInMs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restart(InjectionContext context) {
|
||||
LOG.warn("destroy injection context");
|
||||
context.destroy();
|
||||
|
||||
LOG.warn("send recreate eventbus event");
|
||||
ScmEventBus.getInstance().post(new RecreateEventBusEvent());
|
||||
|
||||
// restart context delayed, to avoid timing problems
|
||||
new Thread(() -> {
|
||||
try {
|
||||
Thread.sleep(waitInMs);
|
||||
|
||||
LOG.warn("reinitialize injection context");
|
||||
context.initialize();
|
||||
|
||||
LOG.debug("register injection context on new eventbus");
|
||||
ScmEventBus.getInstance().register(context);
|
||||
} catch ( Exception ex) {
|
||||
LOG.error("failed to restart", ex);
|
||||
}
|
||||
}, "Delayed-Restart-" + INSTANCE_COUNTER.incrementAndGet()).start();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.inject.Binding;
|
||||
import com.google.inject.Injector;
|
||||
import com.google.inject.Key;
|
||||
import sonia.scm.CloseableModule;
|
||||
import sonia.scm.Default;
|
||||
import sonia.scm.EagerSingletonModule;
|
||||
import sonia.scm.ServletContextListenerHolder;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.servlet.ServletContextEvent;
|
||||
import java.util.Optional;
|
||||
|
||||
class InjectionLifeCycle {
|
||||
|
||||
private final Injector injector;
|
||||
|
||||
InjectionLifeCycle(Injector injector) {
|
||||
this.injector = injector;
|
||||
}
|
||||
|
||||
void initialize() {
|
||||
initializeEagerSingletons();
|
||||
initializeServletContextListeners();
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
destroyServletContextListeners();
|
||||
closeRegisteredCloseables();
|
||||
}
|
||||
|
||||
private void initializeServletContextListeners() {
|
||||
ServletContextListenerHolder instance = injector.getInstance(ServletContextListenerHolder.class);
|
||||
ServletContext context = injector.getInstance(Key.get(ServletContext.class, Default.class));
|
||||
instance.contextInitialized(new ServletContextEvent(context));
|
||||
}
|
||||
|
||||
private void initializeEagerSingletons() {
|
||||
findInstance(EagerSingletonModule.class).ifPresent(m -> m.initialize(injector));
|
||||
}
|
||||
|
||||
private void closeRegisteredCloseables() {
|
||||
findInstance(CloseableModule.class).ifPresent(CloseableModule::closeAll);
|
||||
}
|
||||
|
||||
private void destroyServletContextListeners() {
|
||||
ServletContextListenerHolder instance = injector.getInstance(ServletContextListenerHolder.class);
|
||||
ServletContext context = injector.getInstance(Key.get(ServletContext.class, Default.class));
|
||||
instance.contextDestroyed(new ServletContextEvent(context));
|
||||
}
|
||||
|
||||
private <T> Optional<T> findInstance(Class<T> clazz) {
|
||||
Binding<T> binding = injector.getExistingBinding(Key.get(clazz));
|
||||
if (binding != null) {
|
||||
return Optional.of(binding.getProvider().get());
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import se.jiderhamn.classloader.leak.prevention.ClassLoaderLeakPreventor;
|
||||
|
||||
/**
|
||||
* Logging adapter for {@link ClassLoaderLeakPreventor}.
|
||||
*/
|
||||
public class LoggingAdapter implements se.jiderhamn.classloader.leak.prevention.Logger {
|
||||
|
||||
@SuppressWarnings("squid:S3416") // suppress "loggers should be named for their enclosing classes" rule
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLeakPreventor.class);
|
||||
|
||||
@Override
|
||||
public void debug(String msg) {
|
||||
LOG.debug(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void info(String msg) {
|
||||
LOG.info(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(String msg) {
|
||||
LOG.warn(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void warn(Throwable t) {
|
||||
LOG.warn(t.getMessage(), t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(String msg) {
|
||||
LOG.error(msg);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(Throwable t) {
|
||||
LOG.error(t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.inject.Module;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface ModuleProvider {
|
||||
|
||||
Collection<Module> createModules();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.common.base.Charsets;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.io.Files;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.migration.UpdateException;
|
||||
import sonia.scm.plugin.DefaultPluginLoader;
|
||||
import sonia.scm.plugin.Plugin;
|
||||
import sonia.scm.plugin.PluginException;
|
||||
import sonia.scm.plugin.PluginLoadException;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
import sonia.scm.plugin.PluginWrapper;
|
||||
import sonia.scm.plugin.PluginsInternal;
|
||||
import sonia.scm.plugin.SmpArchive;
|
||||
import sonia.scm.util.IOUtil;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import javax.xml.bind.DataBindingException;
|
||||
import javax.xml.bind.JAXB;
|
||||
import javax.xml.bind.annotation.XmlAccessType;
|
||||
import javax.xml.bind.annotation.XmlAccessorType;
|
||||
import javax.xml.bind.annotation.XmlElement;
|
||||
import javax.xml.bind.annotation.XmlRootElement;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class PluginBootstrap {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PluginBootstrap.class);
|
||||
|
||||
private static final String DIRECTORY_PLUGINS = "plugins";
|
||||
private static final String PLUGIN_DIRECTORY = "/WEB-INF/plugins/";
|
||||
private static final String PLUGIN_COREINDEX = PLUGIN_DIRECTORY.concat("plugin-index.xml");
|
||||
|
||||
private final ClassLoaderLifeCycle classLoaderLifeCycle;
|
||||
private final ServletContext servletContext;
|
||||
private final Set<PluginWrapper> plugins;
|
||||
private final PluginLoader pluginLoader;
|
||||
|
||||
PluginBootstrap(ServletContext servletContext, ClassLoaderLifeCycle classLoaderLifeCycle) {
|
||||
this.servletContext = servletContext;
|
||||
this.classLoaderLifeCycle = classLoaderLifeCycle;
|
||||
|
||||
this.plugins = collectPlugins();
|
||||
this.pluginLoader = createPluginLoader();
|
||||
}
|
||||
|
||||
public PluginLoader getPluginLoader() {
|
||||
return pluginLoader;
|
||||
}
|
||||
|
||||
public Set<PluginWrapper> getPlugins() {
|
||||
return plugins;
|
||||
}
|
||||
|
||||
private PluginLoader createPluginLoader() {
|
||||
return new DefaultPluginLoader(servletContext, classLoaderLifeCycle.getBootstrapClassLoader(), plugins);
|
||||
}
|
||||
|
||||
private Set<PluginWrapper> collectPlugins() {
|
||||
try {
|
||||
File pluginDirectory = getPluginDirectory();
|
||||
|
||||
renameOldPluginsFolder(pluginDirectory);
|
||||
|
||||
if (!isCorePluginExtractionDisabled()) {
|
||||
extractCorePlugins(servletContext, pluginDirectory);
|
||||
} else {
|
||||
LOG.info("core plugin extraction is disabled");
|
||||
}
|
||||
|
||||
return PluginsInternal.collectPlugins(classLoaderLifeCycle, pluginDirectory.toPath());
|
||||
} catch (IOException ex) {
|
||||
throw new PluginLoadException("could not load plugins", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private void renameOldPluginsFolder(File pluginDirectory) {
|
||||
if (new File(pluginDirectory, "classpath.xml").exists()) {
|
||||
File backupDirectory = new File(pluginDirectory.getParentFile(), "plugins.v1");
|
||||
boolean renamed = pluginDirectory.renameTo(backupDirectory);
|
||||
if (renamed) {
|
||||
LOG.warn("moved old plugins directory to {}", backupDirectory);
|
||||
} else {
|
||||
throw new UpdateException("could not rename existing v1 plugin directory");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private boolean isCorePluginExtractionDisabled() {
|
||||
return Boolean.getBoolean("sonia.scm.boot.disable-core-plugin-extraction");
|
||||
}
|
||||
|
||||
private void extractCorePlugin(ServletContext context, File pluginDirectory,
|
||||
PluginIndexEntry entry) throws IOException {
|
||||
URL url = context.getResource(PLUGIN_DIRECTORY.concat(entry.getName()));
|
||||
SmpArchive archive = SmpArchive.create(url);
|
||||
Plugin plugin = archive.getPlugin();
|
||||
|
||||
File directory = PluginsInternal.createPluginDirectory(pluginDirectory, plugin);
|
||||
File checksumFile = PluginsInternal.getChecksumFile(directory);
|
||||
|
||||
if (!directory.exists()) {
|
||||
LOG.warn("install plugin {}", plugin.getInformation().getId());
|
||||
PluginsInternal.extract(archive, entry.getChecksum(), directory, checksumFile, true);
|
||||
} else if (!checksumFile.exists()) {
|
||||
LOG.warn("plugin directory {} exists without checksum file.", directory);
|
||||
PluginsInternal.extract(archive, entry.getChecksum(), directory, checksumFile, true);
|
||||
} else {
|
||||
String checksum = Files.toString(checksumFile, Charsets.UTF_8).trim();
|
||||
|
||||
if (checksum.equals(entry.getChecksum())) {
|
||||
LOG.debug("plugin {} is up to date", plugin.getInformation().getId());
|
||||
} else {
|
||||
LOG.warn("checksum mismatch of pluing {}, start update", plugin.getInformation().getId());
|
||||
PluginsInternal.extract(archive, entry.getChecksum(), directory, checksumFile, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void extractCorePlugins(ServletContext context, File pluginDirectory) throws IOException {
|
||||
IOUtil.mkdirs(pluginDirectory);
|
||||
|
||||
PluginIndex index = readCorePluginIndex(context);
|
||||
|
||||
for (PluginIndexEntry entry : index) {
|
||||
extractCorePlugin(context, pluginDirectory, entry);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private PluginIndex readCorePluginIndex(ServletContext context) {
|
||||
PluginIndex index;
|
||||
|
||||
try {
|
||||
URL indexUrl = context.getResource(PLUGIN_COREINDEX);
|
||||
|
||||
if (indexUrl == null) {
|
||||
throw new PluginException("no core plugin index found");
|
||||
}
|
||||
|
||||
index = JAXB.unmarshal(indexUrl, PluginIndex.class);
|
||||
} catch (MalformedURLException ex) {
|
||||
throw new PluginException("could not load core plugin index", ex);
|
||||
} catch (DataBindingException ex) {
|
||||
throw new PluginException("could not unmarshal core plugin index", ex);
|
||||
}
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
private File getPluginDirectory() {
|
||||
File baseDirectory = SCMContext.getContext().getBaseDirectory();
|
||||
|
||||
return new File(baseDirectory, DIRECTORY_PLUGINS);
|
||||
}
|
||||
|
||||
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
@XmlRootElement(name = "plugin-index")
|
||||
private static class PluginIndex implements Iterable<PluginIndexEntry> {
|
||||
|
||||
@XmlElement(name = "plugins")
|
||||
private List<PluginIndexEntry> plugins;
|
||||
|
||||
@Override
|
||||
public Iterator<PluginIndexEntry> iterator() {
|
||||
return getPlugins().iterator();
|
||||
}
|
||||
|
||||
public List<PluginIndexEntry> getPlugins() {
|
||||
if (plugins == null) {
|
||||
plugins = ImmutableList.of();
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@XmlRootElement(name = "plugins")
|
||||
@XmlAccessorType(XmlAccessType.FIELD)
|
||||
private static class PluginIndexEntry {
|
||||
|
||||
private String checksum;
|
||||
|
||||
private String name;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getChecksum() {
|
||||
return checksum;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.Priority;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.Stage;
|
||||
import sonia.scm.event.ScmEventBus;
|
||||
import sonia.scm.filter.WebElement;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* This servlet sends a {@link RestartEvent} to the {@link ScmEventBus} which causes scm-manager to restart the context.
|
||||
* The {@link RestartServlet} can be used for reloading java code or for installing plugins without a complete restart.
|
||||
* At the moment the Servlet accepts only request, if scm-manager was started in the {@link Stage#DEVELOPMENT} stage.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
@Priority(0)
|
||||
@WebElement("/restart")
|
||||
public class RestartServlet extends HttpServlet {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(RestartServlet.class);
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
private final AtomicBoolean restarting = new AtomicBoolean();
|
||||
|
||||
private final ScmEventBus eventBus;
|
||||
private final Stage stage;
|
||||
|
||||
@Inject
|
||||
public RestartServlet() {
|
||||
this(ScmEventBus.getInstance(), SCMContext.getContext().getStage());
|
||||
}
|
||||
|
||||
RestartServlet(ScmEventBus eventBus, Stage stage) {
|
||||
this.eventBus = eventBus;
|
||||
this.stage = stage;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
|
||||
LOG.info("received sendRestartEvent request");
|
||||
|
||||
if (isRestartAllowed()) {
|
||||
|
||||
try (InputStream requestInput = req.getInputStream()) {
|
||||
Reason reason = objectMapper.readValue(requestInput, Reason.class);
|
||||
sendRestartEvent(resp, reason);
|
||||
} catch (IOException ex) {
|
||||
LOG.warn("failed to trigger sendRestartEvent event", ex);
|
||||
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
} else {
|
||||
LOG.debug("received restart event in non development stage");
|
||||
resp.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRestartAllowed() {
|
||||
return stage == Stage.DEVELOPMENT;
|
||||
}
|
||||
|
||||
private void sendRestartEvent(HttpServletResponse response, Reason reason) {
|
||||
if ( restarting.compareAndSet(false, true) ) {
|
||||
LOG.info("trigger sendRestartEvent, because of {}", reason.getMessage());
|
||||
eventBus.post(new RestartEvent(RestartServlet.class, reason.getMessage()));
|
||||
|
||||
response.setStatus(HttpServletResponse.SC_ACCEPTED);
|
||||
} else {
|
||||
LOG.warn("scm-manager restarts already");
|
||||
response.setStatus(HttpServletResponse.SC_CONFLICT);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Reason {
|
||||
|
||||
private String message;
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
/**
|
||||
* Strategy for restarting SCM-Manager.
|
||||
*/
|
||||
public interface RestartStrategy {
|
||||
|
||||
/**
|
||||
* Context for Injection in SCM-Manager.
|
||||
*/
|
||||
interface InjectionContext {
|
||||
/**
|
||||
* Initialize the injection context.
|
||||
*/
|
||||
void initialize();
|
||||
|
||||
/**
|
||||
* Destroys the injection context.
|
||||
*/
|
||||
void destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart SCM-Manager.
|
||||
* @param context injection context
|
||||
*/
|
||||
void restart(InjectionContext context);
|
||||
|
||||
/**
|
||||
* Returns the configured strategy.
|
||||
*
|
||||
* @return configured strategy
|
||||
*/
|
||||
static RestartStrategy get() {
|
||||
return new InjectionContextRestartStrategy();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
import java.util.Enumeration;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Remove cached resources from {@link ServletContext} to allow a clean restart of scm-manager without stale or
|
||||
* duplicated data.
|
||||
*
|
||||
* @since 2.0.0
|
||||
*/
|
||||
final class ServletContextCleaner {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(ServletContextCleaner.class);
|
||||
|
||||
private static final Set<String> REMOVE_PREFIX = ImmutableSet.of(
|
||||
"org.jboss.resteasy",
|
||||
"resteasy",
|
||||
"org.apache.shiro",
|
||||
"sonia.scm"
|
||||
);
|
||||
|
||||
private ServletContextCleaner() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove cached attributes from {@link ServletContext}.
|
||||
*
|
||||
* @param servletContext servlet context
|
||||
*/
|
||||
static void cleanup(ServletContext servletContext) {
|
||||
LOG.info("remove cached attributes from context");
|
||||
|
||||
Enumeration<String> attributeNames = servletContext.getAttributeNames();
|
||||
while( attributeNames.hasMoreElements()) {
|
||||
String name = attributeNames.nextElement();
|
||||
if (shouldRemove(name)) {
|
||||
LOG.info("remove attribute {} from servlet context", name);
|
||||
servletContext.removeAttribute(name);
|
||||
} else {
|
||||
LOG.info("keep attribute {} in servlet context", name);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private static boolean shouldRemove(String name) {
|
||||
for (String prefix : REMOVE_PREFIX) {
|
||||
if (name.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.inject.servlet.ServletModule;
|
||||
import sonia.scm.Default;
|
||||
|
||||
import javax.servlet.ServletContext;
|
||||
|
||||
class ServletContextModule extends ServletModule {
|
||||
|
||||
@Override
|
||||
protected void configureServlets() {
|
||||
bind(ServletContext.class).annotatedWith(Default.class).toInstance(getServletContext());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.apache.shiro.authc.credential.PasswordService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.plugin.Extension;
|
||||
import sonia.scm.security.PermissionAssigner;
|
||||
import sonia.scm.security.PermissionDescriptor;
|
||||
import sonia.scm.user.User;
|
||||
import sonia.scm.user.UserManager;
|
||||
import sonia.scm.web.security.AdministrationContext;
|
||||
import sonia.scm.web.security.PrivilegedAction;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.servlet.ServletContextEvent;
|
||||
import javax.servlet.ServletContextListener;
|
||||
import java.util.Collections;
|
||||
|
||||
@Extension
|
||||
public class SetupContextListener implements ServletContextListener {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SetupContextListener.class);
|
||||
|
||||
private final AdministrationContext administrationContext;
|
||||
|
||||
@Inject
|
||||
public SetupContextListener(AdministrationContext administrationContext) {
|
||||
this.administrationContext = administrationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextInitialized(ServletContextEvent sce) {
|
||||
if (Boolean.getBoolean("sonia.scm.skipAdminCreation")) {
|
||||
LOG.info("found skipAdminCreation flag; skipping creation of scmadmin");
|
||||
} else {
|
||||
administrationContext.runAsAdmin(SetupAction.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contextDestroyed(ServletContextEvent sce) {}
|
||||
|
||||
@VisibleForTesting
|
||||
static class SetupAction implements PrivilegedAction {
|
||||
|
||||
private final UserManager userManager;
|
||||
private final PasswordService passwordService;
|
||||
private final PermissionAssigner permissionAssigner;
|
||||
|
||||
@Inject
|
||||
public SetupAction(UserManager userManager, PasswordService passwordService, PermissionAssigner permissionAssigner) {
|
||||
this.userManager = userManager;
|
||||
this.passwordService = passwordService;
|
||||
this.permissionAssigner = permissionAssigner;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (isFirstStart()) {
|
||||
createAdminAccount();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isFirstStart() {
|
||||
return userManager.getAll().isEmpty();
|
||||
}
|
||||
|
||||
private void createAdminAccount() {
|
||||
User scmadmin = new User("scmadmin", "SCM Administrator", "scm-admin@scm-manager.org");
|
||||
String password = passwordService.encryptPassword("scmadmin");
|
||||
scmadmin.setPassword(password);
|
||||
userManager.create(scmadmin);
|
||||
|
||||
PermissionDescriptor descriptor = new PermissionDescriptor("*");
|
||||
permissionAssigner.setPermissionsForUser("scmadmin", Collections.singleton(descriptor));
|
||||
}
|
||||
}
|
||||
}
|
||||
115
scm-webapp/src/main/java/sonia/scm/lifecycle/SingleView.java
Normal file
115
scm-webapp/src/main/java/sonia/scm/lifecycle/SingleView.java
Normal file
@@ -0,0 +1,115 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.inject.Module;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import com.google.inject.servlet.ServletModule;
|
||||
import sonia.scm.Default;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.template.MustacheTemplateEngine;
|
||||
import sonia.scm.template.TemplateEngine;
|
||||
import sonia.scm.template.TemplateEngineFactory;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.Collection;
|
||||
|
||||
final class SingleView {
|
||||
|
||||
private SingleView() {
|
||||
}
|
||||
|
||||
static SingleViewModuleProvider error(Throwable throwable) {
|
||||
String error = Throwables.getStackTraceAsString(throwable);
|
||||
|
||||
ViewController controller = new SimpleViewController("/templates/error.mustache", request -> {
|
||||
Object model = ImmutableMap.of(
|
||||
"contextPath", request.getContextPath(),
|
||||
"error", error
|
||||
);
|
||||
return new View(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, model);
|
||||
});
|
||||
return new SingleViewModuleProvider(controller);
|
||||
}
|
||||
|
||||
static SingleViewModuleProvider view(String template, int sc) {
|
||||
ViewController controller = new SimpleViewController(template, request -> {
|
||||
Object model = ImmutableMap.of(
|
||||
"contextPath", request.getContextPath()
|
||||
);
|
||||
return new View(sc, model);
|
||||
});
|
||||
return new SingleViewModuleProvider(controller);
|
||||
}
|
||||
|
||||
private static class SingleViewModuleProvider implements ModuleProvider {
|
||||
|
||||
private final ViewController controller;
|
||||
|
||||
private SingleViewModuleProvider(ViewController controller) {
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<Module> createModules() {
|
||||
return ImmutableList.of(new ServletContextModule(), new SingleViewModule(controller));
|
||||
}
|
||||
}
|
||||
|
||||
private static class SingleViewModule extends ServletModule {
|
||||
|
||||
private final ViewController viewController;
|
||||
|
||||
private SingleViewModule(ViewController viewController) {
|
||||
this.viewController = viewController;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configureServlets() {
|
||||
SCMContextProvider context = SCMContext.getContext();
|
||||
|
||||
bind(SCMContextProvider.class).toInstance(context);
|
||||
bind(ViewController.class).toInstance(viewController);
|
||||
|
||||
Multibinder<TemplateEngine> engineBinder =
|
||||
Multibinder.newSetBinder(binder(), TemplateEngine.class);
|
||||
|
||||
engineBinder.addBinding().to(MustacheTemplateEngine.class);
|
||||
bind(TemplateEngine.class).annotatedWith(Default.class).to(
|
||||
MustacheTemplateEngine.class);
|
||||
bind(TemplateEngineFactory.class);
|
||||
|
||||
serve("/images/*", "/styles/*", "/favicon.ico").with(StaticResourceServlet.class);
|
||||
serve("/*").with(SingleViewServlet.class);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SimpleViewController implements ViewController {
|
||||
|
||||
private final String template;
|
||||
private final SimpleViewFactory viewFactory;
|
||||
|
||||
private SimpleViewController(String template, SimpleViewFactory viewFactory) {
|
||||
this.template = template;
|
||||
this.viewFactory = viewFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTemplate() {
|
||||
return template;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View createView(HttpServletRequest request) {
|
||||
return viewFactory.create(request);
|
||||
}
|
||||
}
|
||||
|
||||
@FunctionalInterface
|
||||
interface SimpleViewFactory {
|
||||
View create(HttpServletRequest request);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.template.Template;
|
||||
import sonia.scm.template.TemplateEngine;
|
||||
import sonia.scm.template.TemplateEngineFactory;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
|
||||
@Singleton
|
||||
public class SingleViewServlet extends HttpServlet {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(SingleViewServlet.class);
|
||||
|
||||
private final Template template;
|
||||
private final ViewController controller;
|
||||
|
||||
@Inject
|
||||
public SingleViewServlet(TemplateEngineFactory templateEngineFactory, ViewController controller) {
|
||||
template = createTemplate(templateEngineFactory, controller.getTemplate());
|
||||
this.controller = controller;
|
||||
}
|
||||
|
||||
private Template createTemplate(TemplateEngineFactory templateEngineFactory, String template) {
|
||||
TemplateEngine engine = templateEngineFactory.getEngineByExtension(template);
|
||||
try {
|
||||
return engine.getTemplate(template);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("failed to parse template: " + template, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
|
||||
process(req, resp);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
|
||||
process(req, resp);
|
||||
}
|
||||
|
||||
private void process(HttpServletRequest request, HttpServletResponse response) {
|
||||
View view = controller.createView(request);
|
||||
|
||||
response.setStatus(view.getStatusCode());
|
||||
response.setContentType("text/html");
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
|
||||
try (PrintWriter writer = response.getWriter()) {
|
||||
template.execute(writer, view.getModel());
|
||||
} catch (IOException ex) {
|
||||
LOG.error("failed to write view", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.github.sdorra.webresources.CacheControl;
|
||||
import com.github.sdorra.webresources.WebResourceSender;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.util.HttpUtil;
|
||||
|
||||
import javax.inject.Singleton;
|
||||
import javax.servlet.http.HttpServlet;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
||||
@Singleton
|
||||
public class StaticResourceServlet extends HttpServlet {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(StaticResourceServlet.class);
|
||||
|
||||
private final WebResourceSender sender = WebResourceSender.create()
|
||||
.withGZIP()
|
||||
.withGZIPMinLength(512)
|
||||
.withBufferSize(16384)
|
||||
.withCacheControl(CacheControl.create().noCache());
|
||||
|
||||
@Override
|
||||
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
|
||||
try {
|
||||
URL resource = createResourceUrlFromRequest(request);
|
||||
if (resource != null) {
|
||||
sender.resource(resource).get(request, response);
|
||||
} else {
|
||||
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
LOG.warn("failed to servce resource", ex);
|
||||
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private URL createResourceUrlFromRequest(HttpServletRequest request) throws MalformedURLException {
|
||||
String uri = HttpUtil.getStrippedURI(request);
|
||||
return request.getServletContext().getResource(uri);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.multibindings.Multibinder;
|
||||
import sonia.scm.migration.UpdateStep;
|
||||
import sonia.scm.plugin.PluginLoader;
|
||||
|
||||
class UpdateStepModule extends AbstractModule {
|
||||
|
||||
private final PluginLoader pluginLoader;
|
||||
|
||||
UpdateStepModule(PluginLoader pluginLoader) {
|
||||
this.pluginLoader = pluginLoader;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure() {
|
||||
Multibinder<UpdateStep> updateStepBinder = Multibinder.newSetBinder(binder(), UpdateStep.class);
|
||||
pluginLoader
|
||||
.getExtensionProcessor()
|
||||
.byExtensionPoint(UpdateStep.class)
|
||||
.forEach(stepClass -> updateStepBinder.addBinding().to(stepClass));
|
||||
}
|
||||
}
|
||||
77
scm-webapp/src/main/java/sonia/scm/lifecycle/Versions.java
Normal file
77
scm-webapp/src/main/java/sonia/scm/lifecycle/Versions.java
Normal file
@@ -0,0 +1,77 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import sonia.scm.SCMContext;
|
||||
import sonia.scm.SCMContextProvider;
|
||||
import sonia.scm.util.IOUtil;
|
||||
import sonia.scm.version.Version;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Optional;
|
||||
|
||||
class Versions {
|
||||
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Versions.class);
|
||||
|
||||
private static final Version MIN_VERSION = Version.parse("1.60");
|
||||
|
||||
private final SCMContextProvider contextProvider;
|
||||
|
||||
@VisibleForTesting
|
||||
Versions(SCMContextProvider contextProvider) {
|
||||
this.contextProvider = contextProvider;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
boolean isPreviousVersionTooOld() {
|
||||
return readVersion().map(v -> v.isOlder(MIN_VERSION)).orElse(false);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
void writeNewVersion() {
|
||||
Path config = contextProvider.resolve(Paths.get("config"));
|
||||
IOUtil.mkdirs(config.toFile());
|
||||
|
||||
String version = contextProvider.getVersion();
|
||||
LOG.debug("write new version {} to file", version);
|
||||
Path versionFile = config.resolve("version.txt");
|
||||
try {
|
||||
Files.write(versionFile, version.getBytes());
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("failed to write version file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Optional<Version> readVersion() {
|
||||
Path versionFile = contextProvider.resolve(Paths.get("config", "version.txt"));
|
||||
if (versionFile.toFile().exists()) {
|
||||
return Optional.of(readVersionFromFile(versionFile));
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private Version readVersionFromFile(Path versionFile) {
|
||||
try {
|
||||
String versionString = new String(Files.readAllBytes(versionFile), StandardCharsets.UTF_8).trim();
|
||||
LOG.debug("read previous version {} from file", versionString);
|
||||
return Version.parse(versionString);
|
||||
} catch (IOException e) {
|
||||
throw new IllegalStateException("failed to read version file", e);
|
||||
}
|
||||
}
|
||||
|
||||
static boolean isTooOld() {
|
||||
return new Versions(SCMContext.getContext()).isPreviousVersionTooOld();
|
||||
}
|
||||
|
||||
static void writeNew() {
|
||||
new Versions(SCMContext.getContext()).writeNewVersion();
|
||||
}
|
||||
|
||||
}
|
||||
20
scm-webapp/src/main/java/sonia/scm/lifecycle/View.java
Normal file
20
scm-webapp/src/main/java/sonia/scm/lifecycle/View.java
Normal file
@@ -0,0 +1,20 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
class View {
|
||||
|
||||
private final int statusCode;
|
||||
private final Object model;
|
||||
|
||||
View(int statusCode, Object model) {
|
||||
this.statusCode = statusCode;
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
Object getModel() {
|
||||
return model;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package sonia.scm.lifecycle;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
public interface ViewController {
|
||||
|
||||
String getTemplate();
|
||||
|
||||
View createView(HttpServletRequest request);
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user