From 56b8dbdb22466c72cf8ef85056cac385a228f23b Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Feb 2020 12:13:10 +0100 Subject: [PATCH 01/18] Reimplement restarting of scm-manager SCM-Manager tries now to figure out which is the best strategy for the restart. It chooses from one of the following strategies: * PosixRestartStrategy which uses native LibC * ExitRestartStrategy uses System.exit and relies on external mechanism to start again * InjectionContextRestartStrategy destroys and re initializes the injection context --- scm-webapp/pom.xml | 14 +++ .../scm/lifecycle/BootstrapContextFilter.java | 9 +- .../java/sonia/scm/lifecycle/CLibrary.java | 22 ++++ .../scm/lifecycle/ExitRestartStrategy.java | 52 +++++++++ .../InjectionContextRestartStrategy.java | 6 +- .../scm/lifecycle/PosixRestartStrategy.java | 52 +++++++++ .../RestartNotSupportedException.java | 10 ++ .../sonia/scm/lifecycle/RestartStrategy.java | 13 ++- .../scm/lifecycle/RestartStrategyFactory.java | 66 ++++++++++++ .../lifecycle/ExitRestartStrategyTest.java | 75 +++++++++++++ .../scm/lifecycle/RestartStrategyTest.java | 101 ++++++++++++++++++ 11 files changed, 412 insertions(+), 8 deletions(-) create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java create mode 100644 scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java create mode 100644 scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index 924e176024..b71d4043c2 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -295,6 +295,20 @@ 2.7.0 + + + + org.kohsuke + akuma + 1.10 + + + + net.java.dev.jna + jna + 5.5.0 + + diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java index 1d642d9c66..388ec7bb73 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/BootstrapContextFilter.java @@ -42,6 +42,7 @@ import sonia.scm.event.ScmEventBus; import javax.servlet.FilterConfig; import javax.servlet.ServletContextEvent; import javax.servlet.ServletException; +import java.util.Optional; //~--- JDK imports ------------------------------------------------------------ @@ -100,8 +101,12 @@ public class BootstrapContextFilter extends GuiceFilter { if (filterConfig == null) { LOG.error("filter config is null, scm-manager is not initialized"); } else { - RestartStrategy restartStrategy = RestartStrategy.get(webAppClassLoader); - restartStrategy.restart(new GuiceInjectionContext()); + Optional restartStrategy = RestartStrategy.get(webAppClassLoader); + if (restartStrategy.isPresent()) { + restartStrategy.get().restart(new GuiceInjectionContext()); + } else { + LOG.warn("restarting is not supported by the underlying platform"); + } } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java new file mode 100644 index 0000000000..91b82ce3d1 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java @@ -0,0 +1,22 @@ +package sonia.scm.lifecycle; + +/** + * Interface for native c library. + */ +@SuppressWarnings({ + "squid:S1214", // usage as constant is common practice for jna + "squid:S1191" // use of sun.* classes is required for jna +}) +interface CLibrary extends com.sun.jna.Library { + CLibrary LIBC = com.sun.jna.Native.load("c", CLibrary.class); + + int F_GETFD = 1; + int F_SETFD = 2; + int FD_CLOEXEC = 1; + + int getdtablesize(); + int fcntl(int fd, int command); + int fcntl(int fd, int command, int flags); + int execvp(String file, com.sun.jna.StringArray args); + String strerror(int errno); +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java new file mode 100644 index 0000000000..dfa3d72f82 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java @@ -0,0 +1,52 @@ +package sonia.scm.lifecycle; + +import com.google.common.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.IntConsumer; + +/** + * {@link RestartStrategy} which tears down the scm-manager context and + * then exists the java process with {@link System#exit(int)}. + *

+ * This is useful if an external mechanism is able to restart the process after it has exited. + */ +class ExitRestartStrategy implements RestartStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(ExitRestartStrategy.class); + + static final String NAME = "exit"; + + static final String PROPERTY_EXIT_CODE = "sonia.scm.restart.exit-code"; + + private IntConsumer exiter = System::exit; + + ExitRestartStrategy() { + } + + @VisibleForTesting + void setExiter(IntConsumer exiter) { + this.exiter = exiter; + } + + @Override + public void restart(InjectionContext context) { + int exitCode = determineExitCode(); + + LOG.warn("destroy injection context"); + context.destroy(); + + LOG.warn("exit scm-manager with exit code {}", exitCode); + exiter.accept(exitCode); + } + + private int determineExitCode() { + String exitCodeAsString = System.getProperty(PROPERTY_EXIT_CODE, "0"); + try { + return Integer.parseInt(exitCodeAsString); + } catch (NumberFormatException ex) { + throw new RestartNotSupportedException("invalid exit code " + exitCodeAsString, ex); + } + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java index 2db60580b1..e78e7b615c 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java @@ -10,9 +10,11 @@ import sonia.scm.event.ShutdownEventBusEvent; import java.util.concurrent.atomic.AtomicLong; /** - * Restart strategy implementation which destroy the injection context and re initialize it. + * Restart strategy which tries to free, every resource used by the context, starts gc and re initializes the context. */ -public class InjectionContextRestartStrategy implements RestartStrategy { +class InjectionContextRestartStrategy implements RestartStrategy { + + static final String NAME = "context"; private static final String DISABLE_RESTART_PROPERTY = "sonia.scm.restart.disable"; private static final String WAIT_PROPERTY = "sonia.scm.restart.wait"; diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java new file mode 100644 index 0000000000..d7d64b9e6d --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java @@ -0,0 +1,52 @@ +package sonia.scm.lifecycle; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static sonia.scm.lifecycle.CLibrary.*; + +/** + * Restart strategy which uses execvp from libc. This strategy is only supported on posix base operating systems. + */ +class PosixRestartStrategy implements RestartStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(PosixRestartStrategy.class); + + PosixRestartStrategy() { + } + + @Override + public void restart(InjectionContext context) { + LOG.warn("destroy injection context"); + context.destroy(); + + LOG.warn("restart scm-manager jvm process"); + try { + restart(); + } catch (IOException e) { + LOG.error("failed to collect java vm arguments", e); + LOG.error("we will now exit the java process"); + System.exit(1); + } + } + + @SuppressWarnings("squid:S1191") // use of sun.* classes is required for jna) + private static void restart() throws IOException { + com.sun.akuma.JavaVMArguments args = com.sun.akuma.JavaVMArguments.current(); + args.remove("--daemon"); + + int sz = LIBC.getdtablesize(); + for(int i=3; i get(ClassLoader webAppClassLoader) { + return Optional.ofNullable(RestartStrategyFactory.create(webAppClassLoader)); } } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java new file mode 100644 index 0000000000..a6fb371aa7 --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java @@ -0,0 +1,66 @@ +package sonia.scm.lifecycle; + +import com.google.common.base.Strings; +import sonia.scm.PlatformType; +import sonia.scm.util.SystemUtil; + +final class RestartStrategyFactory { + + /** + * System property to load a specific restart strategy. + */ + static final String PROPERTY_STRATEGY = "sonia.scm.lifecycle.restart-strategy"; + + /** + * No restart supported. + */ + static final String STRATEGY_NONE = "none"; + + private RestartStrategyFactory() { + } + + /** + * Returns the configured strategy or {@code null} if restart is not supported. + * + * @param webAppClassLoader root webapp classloader + * @return configured strategy or {@code null} + */ + static RestartStrategy create(ClassLoader webAppClassLoader) { + String property = System.getProperty(PROPERTY_STRATEGY); + if (Strings.isNullOrEmpty(property)) { + return forPlatform(); + } + return fromProperty(webAppClassLoader, property); + } + + private static RestartStrategy fromProperty(ClassLoader webAppClassLoader, String property) { + if (STRATEGY_NONE.equalsIgnoreCase(property)) { + return null; + } else if (ExitRestartStrategy.NAME.equalsIgnoreCase(property)) { + return new ExitRestartStrategy(); + } else if (InjectionContextRestartStrategy.NAME.equalsIgnoreCase(property)) { + return new InjectionContextRestartStrategy(webAppClassLoader); + } else { + return fromClassName(property); + } + } + + private static RestartStrategy fromClassName(String property) { + try { + Class rsClass = Class.forName(property).asSubclass(RestartStrategy.class); + return rsClass.getConstructor().newInstance(); + } catch (Exception e) { + throw new RestartNotSupportedException("failed to create restart strategy from property", e); + } + } + + private static RestartStrategy forPlatform() { + // we do not use SystemUtil here, to allow testing + String osName = System.getProperty(SystemUtil.PROPERTY_OSNAME); + PlatformType platform = PlatformType.createPlatformType(osName); + if (platform.isPosix()) { + return new PosixRestartStrategy(); + } + return null; + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java new file mode 100644 index 0000000000..078e218b75 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java @@ -0,0 +1,75 @@ +package sonia.scm.lifecycle; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.function.IntConsumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ExitRestartStrategyTest { + + @Mock + private RestartStrategy.InjectionContext context; + + private ExitRestartStrategy strategy; + private CapturingExiter exiter; + + @BeforeEach + void setUpStrategy() { + strategy = new ExitRestartStrategy(); + exiter = new CapturingExiter(); + strategy.setExiter(exiter); + } + + @Test + void shouldTearDownContextAndThenExit() { + strategy.restart(context); + + verify(context).destroy(); + assertThat(exiter.getExitCode()).isEqualTo(0); + } + + @Test + void shouldUseExitCodeFromSystemProperty() { + System.setProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE, "42"); + try { + strategy.restart(context); + + verify(context).destroy(); + assertThat(exiter.getExitCode()).isEqualTo(42); + } finally { + System.clearProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE); + } + } + + @Test + void shouldThrowExceptionForNonNumericExitCode() { + System.setProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE, "xyz"); + try { + assertThrows(RestartNotSupportedException.class, () -> strategy.restart(context)); + } finally { + System.clearProperty(ExitRestartStrategy.PROPERTY_EXIT_CODE); + } + } + + private static class CapturingExiter implements IntConsumer { + + private int exitCode = -1; + + public int getExitCode() { + return exitCode; + } + + @Override + public void accept(int exitCode) { + this.exitCode = exitCode; + } + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java new file mode 100644 index 0000000000..914c506aa2 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java @@ -0,0 +1,101 @@ +package sonia.scm.lifecycle; + +import com.google.common.base.Strings; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import sonia.scm.util.SystemUtil; + +import java.util.Optional; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class RestartStrategyTest { + private final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + @Test + void shouldReturnRestartStrategyFromSystemProperty() { + withStrategy(TestingRestartStrategy.class.getName(), (rs) -> { + assertThat(rs).containsInstanceOf(TestingRestartStrategy.class); + }); + } + + @Test + void shouldThrowExceptionForNonStrategyClass() { + withStrategy(RestartStrategyTest.class.getName(), () -> { + assertThrows(RestartNotSupportedException.class, () -> RestartStrategy.get(classLoader)); + }); + } + + @Test + void shouldReturnEmpty() { + withStrategy(RestartStrategyFactory.STRATEGY_NONE, (rs) -> { + assertThat(rs).isEmpty(); + }); + } + + @Test + void shouldReturnEmptyForUnknownOs() { + withSystemProperty(SystemUtil.PROPERTY_OSNAME, "hitchhiker-os", () -> { + Optional restartStrategy = RestartStrategy.get(classLoader); + assertThat(restartStrategy).isEmpty(); + }); + } + + @Test + void shouldReturnExitRestartStrategy() { + withStrategy(ExitRestartStrategy.NAME, (rs) -> { + assertThat(rs).containsInstanceOf(ExitRestartStrategy.class); + }); + } + + @Test + void shouldReturnInjectionContextRestartStrategy() { + withStrategy(InjectionContextRestartStrategy.NAME, (rs) -> { + assertThat(rs).containsInstanceOf(InjectionContextRestartStrategy.class); + }); + } + + @ParameterizedTest + @ValueSource(strings = { "linux", "darwin", "solaris", "freebsd", "openbsd" }) + void shouldReturnPosixRestartStrategyForPosixBased(String os) { + withSystemProperty(SystemUtil.PROPERTY_OSNAME, os, () -> { + Optional restartStrategy = RestartStrategy.get(classLoader); + assertThat(restartStrategy).containsInstanceOf(PosixRestartStrategy.class); + }); + } + + private void withStrategy(String strategy, Consumer> consumer) { + withStrategy(strategy, () -> { + consumer.accept(RestartStrategy.get(classLoader)); + }); + } + + private void withStrategy(String strategy, Runnable runnable) { + withSystemProperty(RestartStrategyFactory.PROPERTY_STRATEGY, strategy, runnable); + } + + private void withSystemProperty(String key, String value, Runnable runnable) { + String oldValue = System.getProperty(key); + System.setProperty(key, value); + try { + runnable.run(); + } finally { + if (Strings.isNullOrEmpty(oldValue)) { + System.clearProperty(key); + } else { + System.setProperty(key, oldValue); + } + } + } + + public static class TestingRestartStrategy implements RestartStrategy { + @Override + public void restart(InjectionContext context) { + + } + } + +} From bca34b829dac43bc654f9a4e40ef59a69bd09759 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Feb 2020 12:36:37 +0100 Subject: [PATCH 02/18] use SimpleClassLoaderLifeCycle by default --- scm-it/pom.xml | 4 ---- scm-webapp/pom.xml | 8 -------- .../lifecycle/InjectionContextRestartStrategy.java | 2 ++ .../classloading/ClassLoaderLifeCycle.java | 12 ++++++------ .../ClassLoaderLifeCycleWithLeakPrevention.java | 2 ++ .../classloading/ClassLoaderLifeCycleTest.java | 13 ++++++++++++- 6 files changed, 22 insertions(+), 19 deletions(-) diff --git a/scm-it/pom.xml b/scm-it/pom.xml index 9064a39fc1..c38c73ce3e 100644 --- a/scm-it/pom.xml +++ b/scm-it/pom.xml @@ -200,10 +200,6 @@ java.awt.headless true - - sonia.scm.classloading.lifecycle - simple - /scm diff --git a/scm-webapp/pom.xml b/scm-webapp/pom.xml index b71d4043c2..ba3805b094 100644 --- a/scm-webapp/pom.xml +++ b/scm-webapp/pom.xml @@ -734,10 +734,6 @@ scm.stage ${scm.stage} - - sonia.scm.classloading.lifecycle - simple - ${project.basedir}/src/main/conf/jetty.xml 0 @@ -823,10 +819,6 @@ scm.home target/scm-it - - sonia.scm.classloading.lifecycle - simple - ${project.basedir}/src/main/conf/jetty.xml 0 diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java index e78e7b615c..a2ea3b7f9d 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/InjectionContextRestartStrategy.java @@ -11,6 +11,8 @@ import java.util.concurrent.atomic.AtomicLong; /** * Restart strategy which tries to free, every resource used by the context, starts gc and re initializes the context. + * Warning: This strategy should only be used with an classloader lifecycle which protects the + * created plugin classloader from classloader leaks. */ class InjectionContextRestartStrategy implements RestartStrategy { diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java index fb7d991c1e..683dbc8fbb 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycle.java @@ -19,17 +19,17 @@ public abstract class ClassLoaderLifeCycle implements LifeCycle { private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycle.class); @VisibleForTesting - static final String PROPERTY = "sonia.scm.classloading.lifecycle"; + static final String PROPERTY = "sonia.scm.lifecycle.classloading"; public static ClassLoaderLifeCycle create() { ClassLoader webappClassLoader = Thread.currentThread().getContextClassLoader(); String implementation = System.getProperty(PROPERTY); - if (SimpleClassLoaderLifeCycle.NAME.equalsIgnoreCase(implementation)) { - LOG.info("create new simple ClassLoaderLifeCycle"); - return new SimpleClassLoaderLifeCycle(webappClassLoader); + if (ClassLoaderLifeCycleWithLeakPrevention.NAME.equalsIgnoreCase(implementation)) { + LOG.info("create new ClassLoaderLifeCycle with leak prevention"); + return new ClassLoaderLifeCycleWithLeakPrevention(webappClassLoader); } - LOG.info("create new ClassLoaderLifeCycle with leak prevention"); - return new ClassLoaderLifeCycleWithLeakPrevention(webappClassLoader); + LOG.info("create new simple ClassLoaderLifeCycle"); + return new SimpleClassLoaderLifeCycle(webappClassLoader); } private final ClassLoader webappClassLoader; diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java index e2e7a032b7..471f4ebc9f 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleWithLeakPrevention.java @@ -26,6 +26,8 @@ import static se.jiderhamn.classloader.leak.prevention.cleanup.ShutdownHookClean */ final class ClassLoaderLifeCycleWithLeakPrevention extends ClassLoaderLifeCycle { + public static final String NAME = "with-leak-prevention"; + private static final Logger LOG = LoggerFactory.getLogger(ClassLoaderLifeCycleWithLeakPrevention.class); private Deque classLoaders = new ArrayDeque<>(); diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java index 619b09940e..9ca3a765d1 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/classloading/ClassLoaderLifeCycleTest.java @@ -17,9 +17,20 @@ class ClassLoaderLifeCycleTest { } } + @Test + void shouldCreateWithLeakPreventionClassLoader() { + System.setProperty(ClassLoaderLifeCycle.PROPERTY, ClassLoaderLifeCycleWithLeakPrevention.NAME); + try { + ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create(); + assertThat(classLoaderLifeCycle).isInstanceOf(ClassLoaderLifeCycleWithLeakPrevention.class); + } finally { + System.clearProperty(ClassLoaderLifeCycle.PROPERTY); + } + } + @Test void shouldCreateDefaultClassLoader() { ClassLoaderLifeCycle classLoaderLifeCycle = ClassLoaderLifeCycle.create(); - assertThat(classLoaderLifeCycle).isInstanceOf(ClassLoaderLifeCycleWithLeakPrevention.class); + assertThat(classLoaderLifeCycle).isInstanceOf(SimpleClassLoaderLifeCycle.class); } } From de3db6252e13502455aa3c743bb460d19c612790 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Wed, 12 Feb 2020 14:45:13 +0100 Subject: [PATCH 03/18] implemented restarter to move control over the restart process to the core --- .../sonia/scm/lifecycle/RestartEvent.java | 42 +++-------- .../RestartNotSupportedException.java | 4 ++ .../java/sonia/scm/lifecycle/Restarter.java | 25 +++++++ .../scm/lifecycle/RestartEventFactory.java | 15 ++++ .../sonia/scm/lifecycle/DefaultRestarter.java | 41 +++++++++++ .../lifecycle/modules/BootstrapModule.java | 4 ++ .../scm/plugin/DefaultPluginManager.java | 20 ++---- .../scm/update/MigrationWizardServlet.java | 12 +++- .../scm/lifecycle/DefaultRestarterTest.java | 69 +++++++++++++++++++ .../scm/plugin/DefaultPluginManagerTest.java | 30 ++++---- .../update/MigrationWizardServletTest.java | 5 +- .../sonia/scm/web/i18n/I18nServletTest.java | 3 +- 12 files changed, 201 insertions(+), 69 deletions(-) rename {scm-webapp => scm-core}/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java (80%) create mode 100644 scm-core/src/main/java/sonia/scm/lifecycle/Restarter.java create mode 100644 scm-test/src/main/java/sonia/scm/lifecycle/RestartEventFactory.java create mode 100644 scm-webapp/src/main/java/sonia/scm/lifecycle/DefaultRestarter.java create mode 100644 scm-webapp/src/test/java/sonia/scm/lifecycle/DefaultRestarterTest.java diff --git a/scm-core/src/main/java/sonia/scm/lifecycle/RestartEvent.java b/scm-core/src/main/java/sonia/scm/lifecycle/RestartEvent.java index 1978cb9d7c..1b692d8081 100644 --- a/scm-core/src/main/java/sonia/scm/lifecycle/RestartEvent.java +++ b/scm-core/src/main/java/sonia/scm/lifecycle/RestartEvent.java @@ -36,63 +36,37 @@ package sonia.scm.lifecycle; import sonia.scm.event.Event; /** - * This event can be used to force a restart of the webapp context. The restart - * event is useful during plugin development, because we don't have to restart - * the whole server, to see our changes. The restart event could also be used - * to install or upgrade plugins. - * - * But the restart event should be used carefully, because the whole context - * will be restarted and that process could take some time. - * + * This event indicates a forced restart of scm-manager. * @author Sebastian Sdorra * @since 2.0.0 */ @Event -public class RestartEvent -{ +public class RestartEvent { - /** - * Constructs ... - * - * - * @param cause - * @param reason - */ - public RestartEvent(Class cause, String reason) - { + private final Class cause; + private final String reason; + + RestartEvent(Class cause, String reason) { this.cause = cause; this.reason = reason; } - //~--- get methods ---------------------------------------------------------- - /** * The class which has fired the restart event. * - * * @return class which has fired the restart event */ - public Class getCause() - { + public Class getCause() { return cause; } /** * Returns the reason for the restart. * - * * @return reason for restart */ - public String getReason() - { + public String getReason() { return reason; } - //~--- fields --------------------------------------------------------------- - - /** cause of restart */ - private final Class cause; - - /** reason for restart */ - private final String reason; } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java b/scm-core/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java similarity index 80% rename from scm-webapp/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java rename to scm-core/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java index 53ab75ff90..062f639096 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java +++ b/scm-core/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java @@ -4,6 +4,10 @@ package sonia.scm.lifecycle; * Exception is thrown if a restart is not supported or a restart strategy is misconfigured. */ public class RestartNotSupportedException extends RuntimeException { + RestartNotSupportedException(String message) { + super(message); + } + RestartNotSupportedException(String message, Throwable cause) { super(message, cause); } diff --git a/scm-core/src/main/java/sonia/scm/lifecycle/Restarter.java b/scm-core/src/main/java/sonia/scm/lifecycle/Restarter.java new file mode 100644 index 0000000000..547d26e391 --- /dev/null +++ b/scm-core/src/main/java/sonia/scm/lifecycle/Restarter.java @@ -0,0 +1,25 @@ +package sonia.scm.lifecycle; + +/** + * {@link Restarter} is able to restart scm-manager. + * + * @since 2.0.0 + */ +public interface Restarter { + + /** + * Return {@code true} if restarting scm-manager is supported. + * + * @return {@code true} if restart is supported + */ + boolean isSupported(); + + /** + * Issues a restart. The method will fire a {@link RestartEvent} to notify the system about the upcoming restart. + * If restarting is not supported by the underlying platform a {@link RestartNotSupportedException} is thrown. + * + * @param cause cause of the restart. This should be the class which calls this method. + * @param reason reason for the required restart. + */ + void restart(Class cause, String reason); +} diff --git a/scm-test/src/main/java/sonia/scm/lifecycle/RestartEventFactory.java b/scm-test/src/main/java/sonia/scm/lifecycle/RestartEventFactory.java new file mode 100644 index 0000000000..59d27fe4c2 --- /dev/null +++ b/scm-test/src/main/java/sonia/scm/lifecycle/RestartEventFactory.java @@ -0,0 +1,15 @@ +package sonia.scm.lifecycle; + +/** + * Creates restart events for testing. + * This is required, because the constructor of {@link RestartEvent} is package private. + */ +public final class RestartEventFactory { + + private RestartEventFactory(){} + + public static RestartEvent create(Class cause, String reason) { + return new RestartEvent(cause, reason); + } + +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/DefaultRestarter.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/DefaultRestarter.java new file mode 100644 index 0000000000..0a787d239a --- /dev/null +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/DefaultRestarter.java @@ -0,0 +1,41 @@ +package sonia.scm.lifecycle; + +import com.google.common.annotations.VisibleForTesting; +import sonia.scm.event.ScmEventBus; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class DefaultRestarter implements Restarter { + + private ScmEventBus eventBus; + private RestartStrategy strategy; + + @Inject + public DefaultRestarter() { + this( + ScmEventBus.getInstance(), + RestartStrategy.get(Thread.currentThread().getContextClassLoader()).orElse(null) + ); + } + + @VisibleForTesting + DefaultRestarter(ScmEventBus eventBus, RestartStrategy strategy) { + this.eventBus = eventBus; + this.strategy = strategy; + } + + @Override + public boolean isSupported() { + return strategy != null; + } + + @Override + public void restart(Class cause, String reason) { + if (!isSupported()) { + throw new RestartNotSupportedException("restarting is not supported"); + } + eventBus.post(new RestartEvent(cause, reason)); + } +} diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java index 06f6462af0..5ad623f8a4 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/modules/BootstrapModule.java @@ -9,6 +9,8 @@ import sonia.scm.SCMContext; import sonia.scm.SCMContextProvider; import sonia.scm.io.DefaultFileSystem; import sonia.scm.io.FileSystem; +import sonia.scm.lifecycle.DefaultRestarter; +import sonia.scm.lifecycle.Restarter; import sonia.scm.plugin.PluginLoader; import sonia.scm.repository.RepositoryLocationResolver; import sonia.scm.repository.xml.MetadataStore; @@ -61,6 +63,8 @@ public class BootstrapModule extends AbstractModule { bind(FileSystem.class, DefaultFileSystem.class); + bind(Restarter.class, DefaultRestarter.class); + // note CipherUtil uses an other generator bind(CipherHandler.class).toInstance(CipherUtil.getInstance().getCipherHandler()); diff --git a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java index f57d932e1e..731b7b12d5 100644 --- a/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java +++ b/scm-webapp/src/main/java/sonia/scm/plugin/DefaultPluginManager.java @@ -41,6 +41,7 @@ import org.slf4j.LoggerFactory; import sonia.scm.NotFoundException; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; +import sonia.scm.lifecycle.Restarter; import sonia.scm.version.Version; import javax.inject.Inject; @@ -68,20 +69,21 @@ public class DefaultPluginManager implements PluginManager { private static final Logger LOG = LoggerFactory.getLogger(DefaultPluginManager.class); - private final ScmEventBus eventBus; private final PluginLoader loader; private final PluginCenter center; private final PluginInstaller installer; + private final Restarter restarter; + private final Collection pendingInstallQueue = new ArrayList<>(); private final Collection pendingUninstallQueue = new ArrayList<>(); private final PluginDependencyTracker dependencyTracker = new PluginDependencyTracker(); @Inject - public DefaultPluginManager(ScmEventBus eventBus, PluginLoader loader, PluginCenter center, PluginInstaller installer) { - this.eventBus = eventBus; + public DefaultPluginManager(PluginLoader loader, PluginCenter center, PluginInstaller installer, Restarter restarter) { this.loader = loader; this.center = center; this.installer = installer; + this.restarter = restarter; this.computeInstallationDependencies(); } @@ -242,16 +244,8 @@ public class DefaultPluginManager implements PluginManager { } } - @VisibleForTesting - void triggerRestart(String cause) { - new Thread(() -> { - try { - Thread.sleep(200); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - eventBus.post(new RestartEvent(PluginManager.class, cause)); - }).start(); + private void triggerRestart(String cause) { + restarter.restart(PluginManager.class, cause); } private void cancelPending(List pendingInstallations) { diff --git a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java index b0ee63c0e8..b518bef698 100644 --- a/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java +++ b/scm-webapp/src/main/java/sonia/scm/update/MigrationWizardServlet.java @@ -12,6 +12,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sonia.scm.event.ScmEventBus; import sonia.scm.lifecycle.RestartEvent; +import sonia.scm.lifecycle.Restarter; import sonia.scm.update.repository.DefaultMigrationStrategyDAO; import sonia.scm.update.repository.MigrationStrategy; import sonia.scm.update.repository.V1Repository; @@ -41,11 +42,13 @@ class MigrationWizardServlet extends HttpServlet { private final XmlRepositoryV1UpdateStep repositoryV1UpdateStep; private final DefaultMigrationStrategyDAO migrationStrategyDao; + private final Restarter restarter; @Inject - MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, DefaultMigrationStrategyDAO migrationStrategyDao) { + MigrationWizardServlet(XmlRepositoryV1UpdateStep repositoryV1UpdateStep, DefaultMigrationStrategyDAO migrationStrategyDao, Restarter restarter) { this.repositoryV1UpdateStep = repositoryV1UpdateStep; this.migrationStrategyDao = migrationStrategyDao; + this.restarter = restarter; } @Override @@ -121,7 +124,12 @@ class MigrationWizardServlet extends HttpServlet { ThreadContext.bind(new Subject.Builder(new DefaultSecurityManager()).authenticated(false).buildSubject()); - ScmEventBus.getInstance().post(new RestartEvent(MigrationWizardServlet.class, "wrote migration data")); + if (restarter.isSupported()) { + restarter.restart(MigrationWizardServlet.class, "wrote migration data"); + } else { + LOG.error("Restarting is not supported on this platform."); + LOG.error("Please do a manual restart"); + } } private List getRepositoryLineEntries() { diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/DefaultRestarterTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/DefaultRestarterTest.java new file mode 100644 index 0000000000..822b7fad64 --- /dev/null +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/DefaultRestarterTest.java @@ -0,0 +1,69 @@ +package sonia.scm.lifecycle; + +import com.github.legman.Subscribe; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.event.ScmEventBus; + +import javax.swing.*; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class DefaultRestarterTest { + + @Mock + private ScmEventBus eventBus; + + @Captor + private ArgumentCaptor eventCaptor; + + @Test + void shouldLoadStrategyOnCreation() { + System.setProperty(RestartStrategyFactory.PROPERTY_STRATEGY, ExitRestartStrategy.NAME); + try { + DefaultRestarter restarter = new DefaultRestarter(); + assertThat(restarter.isSupported()).isTrue(); + } finally { + System.clearProperty(RestartStrategyFactory.PROPERTY_STRATEGY); + } + } + + @Test + void shouldReturnFalseIfRestartStrategyIsNotAvailable() { + DefaultRestarter restarter = new DefaultRestarter(eventBus, null); + assertThat(restarter.isSupported()).isFalse(); + } + + @Test + void shouldReturnTrueIfRestartStrategyIsAvailable() { + DefaultRestarter restarter = new DefaultRestarter(); + assertThat(restarter.isSupported()).isTrue(); + } + + @Test + void shouldThrowRestartNotSupportedException() { + DefaultRestarter restarter = new DefaultRestarter(eventBus,null); + assertThrows( + RestartNotSupportedException.class, () -> restarter.restart(DefaultRestarterTest.class, "test") + ); + } + + @Test + void shouldFireRestartEvent() { + DefaultRestarter restarter = new DefaultRestarter(eventBus, new ExitRestartStrategy()); + restarter.restart(DefaultRestarterTest.class, "testing"); + + verify(eventBus).post(eventCaptor.capture()); + + RestartEvent event = eventCaptor.getValue(); + assertThat(event.getCause()).isEqualTo(DefaultRestarterTest.class); + assertThat(event.getReason()).isEqualTo("testing"); + } +} diff --git a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java index f6aa1d7301..ed575734f2 100644 --- a/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java +++ b/scm-webapp/src/test/java/sonia/scm/plugin/DefaultPluginManagerTest.java @@ -12,10 +12,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import sonia.scm.NotFoundException; import sonia.scm.ScmConstraintViolationException; +import sonia.scm.lifecycle.Restarter; import java.io.IOException; import java.nio.file.Files; @@ -54,13 +56,15 @@ class DefaultPluginManagerTest { @Mock private PluginInstaller installer; + @Mock + private Restarter restarter; + + @InjectMocks private DefaultPluginManager manager; @Mock private Subject subject; - private boolean restartTriggered = false; - @BeforeEach void mockInstaller() { lenient().when(installer.install(any())).then(ic -> { @@ -69,16 +73,6 @@ class DefaultPluginManagerTest { }); } - @BeforeEach - void createPluginManagerToTestWithCapturedRestart() { - manager = new DefaultPluginManager(null, loader, center, installer) { // event bus is only used in restart and this is replaced here - @Override - void triggerRestart(String cause) { - restartTriggered = true; - } - }; - } - @Nested class WithAdminPermissions { @@ -185,7 +179,7 @@ class DefaultPluginManagerTest { manager.install("scm-git-plugin", false); verify(installer).install(git); - assertThat(restartTriggered).isFalse(); + verify(restarter, never()).restart(any(), any()); } @Test @@ -263,7 +257,7 @@ class DefaultPluginManagerTest { manager.install("scm-git-plugin", true); verify(installer).install(git); - assertThat(restartTriggered).isTrue(); + verify(restarter).restart(any(), any()); } @Test @@ -272,7 +266,7 @@ class DefaultPluginManagerTest { when(loader.getInstalledPlugins()).thenReturn(ImmutableList.of(gitInstalled)); manager.install("scm-git-plugin", true); - assertThat(restartTriggered).isFalse(); + verify(restarter, never()).restart(any(), any()); } @Test @@ -294,14 +288,14 @@ class DefaultPluginManagerTest { manager.install("scm-review-plugin", false); manager.executePendingAndRestart(); - assertThat(restartTriggered).isTrue(); + verify(restarter).restart(any(), any()); } @Test void shouldNotSendRestartEventWithoutPendingPlugins() { manager.executePendingAndRestart(); - assertThat(restartTriggered).isFalse(); + verify(restarter, never()).restart(any(), any()); } @Test @@ -452,7 +446,7 @@ class DefaultPluginManagerTest { manager.executePendingAndRestart(); - assertThat(restartTriggered).isTrue(); + verify(restarter).restart(any(), any()); } @Test diff --git a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java index 683c446af7..286e8855f3 100644 --- a/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/update/MigrationWizardServletTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.lifecycle.Restarter; import sonia.scm.update.repository.DefaultMigrationStrategyDAO; import sonia.scm.update.repository.MigrationStrategy; import sonia.scm.update.repository.V1Repository; @@ -27,6 +28,8 @@ class MigrationWizardServletTest { XmlRepositoryV1UpdateStep updateStep; @Mock DefaultMigrationStrategyDAO migrationStrategyDao; + @Mock + Restarter restarter; @Mock HttpServletRequest request; @@ -40,7 +43,7 @@ class MigrationWizardServletTest { @BeforeEach void initServlet() { - servlet = new MigrationWizardServlet(updateStep, migrationStrategyDao) { + servlet = new MigrationWizardServlet(updateStep, migrationStrategyDao, restarter) { @Override void respondWithTemplate(HttpServletResponse resp, Map model, String templateName) { renderedTemplateName = templateName; diff --git a/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java b/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java index f177d44e63..e060d7a2e8 100644 --- a/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java +++ b/scm-webapp/src/test/java/sonia/scm/web/i18n/I18nServletTest.java @@ -20,6 +20,7 @@ import sonia.scm.lifecycle.RestartEvent; import sonia.scm.cache.Cache; import sonia.scm.cache.CacheManager; import sonia.scm.event.ScmEventBus; +import sonia.scm.lifecycle.RestartEventFactory; import sonia.scm.plugin.PluginLoader; import javax.servlet.http.HttpServletRequest; @@ -114,7 +115,7 @@ public class I18nServletTest { public void shouldCleanCacheOnRestartEvent() { ScmEventBus.getInstance().register(servlet); - ScmEventBus.getInstance().post(new RestartEvent(I18nServlet.class, "Restart to reload the plugin resources")); + ScmEventBus.getInstance().post(RestartEventFactory.create(I18nServlet.class, "Restart to reload the plugin resources")); verify(cache).clear(); } From b97dfd7dedd15582cadb271dcadaac644bec3bc9 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 24 Mar 2020 10:19:10 +0100 Subject: [PATCH 04/18] added missing license headers --- .../RestartNotSupportedException.java | 23 +++++++++++++++++++ .../java/sonia/scm/lifecycle/Restarter.java | 23 +++++++++++++++++++ .../scm/lifecycle/RestartEventFactory.java | 23 +++++++++++++++++++ .../java/sonia/scm/lifecycle/CLibrary.java | 23 +++++++++++++++++++ .../sonia/scm/lifecycle/DefaultRestarter.java | 23 +++++++++++++++++++ .../scm/lifecycle/ExitRestartStrategy.java | 23 +++++++++++++++++++ .../scm/lifecycle/PosixRestartStrategy.java | 23 +++++++++++++++++++ .../scm/lifecycle/RestartStrategyFactory.java | 23 +++++++++++++++++++ .../scm/lifecycle/DefaultRestarterTest.java | 23 +++++++++++++++++++ .../lifecycle/ExitRestartStrategyTest.java | 23 +++++++++++++++++++ .../scm/lifecycle/RestartStrategyTest.java | 23 +++++++++++++++++++ 11 files changed, 253 insertions(+) diff --git a/scm-core/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java b/scm-core/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java index 062f639096..7b560f7222 100644 --- a/scm-core/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java +++ b/scm-core/src/main/java/sonia/scm/lifecycle/RestartNotSupportedException.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; /** diff --git a/scm-core/src/main/java/sonia/scm/lifecycle/Restarter.java b/scm-core/src/main/java/sonia/scm/lifecycle/Restarter.java index 547d26e391..0b6c18f0a9 100644 --- a/scm-core/src/main/java/sonia/scm/lifecycle/Restarter.java +++ b/scm-core/src/main/java/sonia/scm/lifecycle/Restarter.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; /** diff --git a/scm-test/src/main/java/sonia/scm/lifecycle/RestartEventFactory.java b/scm-test/src/main/java/sonia/scm/lifecycle/RestartEventFactory.java index 59d27fe4c2..d58897dd40 100644 --- a/scm-test/src/main/java/sonia/scm/lifecycle/RestartEventFactory.java +++ b/scm-test/src/main/java/sonia/scm/lifecycle/RestartEventFactory.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; /** diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java index 91b82ce3d1..cee163eb8b 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/CLibrary.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; /** diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/DefaultRestarter.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/DefaultRestarter.java index 0a787d239a..cb85852f83 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/DefaultRestarter.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/DefaultRestarter.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; import com.google.common.annotations.VisibleForTesting; diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java index dfa3d72f82..d8eb2fd22a 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/ExitRestartStrategy.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; import com.google.common.annotations.VisibleForTesting; diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java index d7d64b9e6d..6d5477dd78 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/PosixRestartStrategy.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; import org.slf4j.Logger; diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java index a6fb371aa7..e522f3ad41 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; import com.google.common.base.Strings; diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/DefaultRestarterTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/DefaultRestarterTest.java index 822b7fad64..a137d7a947 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/DefaultRestarterTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/DefaultRestarterTest.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; import com.github.legman.Subscribe; diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java index 078e218b75..8fba403120 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/ExitRestartStrategyTest.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; import org.junit.jupiter.api.BeforeEach; diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java index 914c506aa2..3301e46151 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java @@ -1,3 +1,26 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package sonia.scm.lifecycle; import com.google.common.base.Strings; From 651a9a561a73775caa686754ba20503373735cd7 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 24 Mar 2020 13:05:41 +0100 Subject: [PATCH 05/18] fix test execution on intellij --- pom.xml | 7 +++++++ scm-test/pom.xml | 16 +++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index c3f6cb139f..c38bb5ef84 100644 --- a/pom.xml +++ b/pom.xml @@ -363,6 +363,13 @@ test + + junit + junit + 4.13 + test + + org.hamcrest hamcrest-core diff --git a/scm-test/pom.xml b/scm-test/pom.xml index 05d773a6f8..a24e048f55 100644 --- a/scm-test/pom.xml +++ b/scm-test/pom.xml @@ -38,7 +38,7 @@ scm-test 2.0.0-SNAPSHOT scm-test - + @@ -53,6 +53,12 @@ 2.0.0-SNAPSHOT + + junit + junit + compile + + com.github.sdorra shiro-unit @@ -88,17 +94,17 @@ - + - + - + maven.tmatesoft.com tmatesoft release repository https://maven.tmatesoft.com/content/repositories/releases - + From 2873c44b5264cc8bbf05bbd9a7532ee9e72105da Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 24 Mar 2020 15:01:39 +0100 Subject: [PATCH 06/18] show restart checkbox only if restarting is supported --- scm-ui/ui-webapp/public/locales/de/admin.json | 3 +- scm-ui/ui-webapp/public/locales/en/admin.json | 3 +- .../admin/plugins/components/PluginModal.tsx | 26 +++++++++++----- .../scm/api/v2/resources/PluginDtoMapper.java | 23 +++++++++++--- .../PluginDtoCollectionMapperTest.java | 23 ++++++++++++-- .../api/v2/resources/PluginDtoMapperTest.java | 31 ++++++++++++++++++- 6 files changed, 93 insertions(+), 16 deletions(-) diff --git a/scm-ui/ui-webapp/public/locales/de/admin.json b/scm-ui/ui-webapp/public/locales/de/admin.json index e65a10eb85..b2ffbfe282 100644 --- a/scm-ui/ui-webapp/public/locales/de/admin.json +++ b/scm-ui/ui-webapp/public/locales/de/admin.json @@ -69,7 +69,8 @@ "restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.", "executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird der SCM-Manager Kontext neu gestartet.", "cancelPending": "Die folgenden Plugin-Änderungen werden abgebrochen und zurückgesetzt.", - "updateAllInfo": "Die folgenden Plugins werden aktualisiert. Die Änderungen werden nach dem nächsten Neustart wirksam." + "updateAllInfo": "Die folgenden Plugins werden aktualisiert. Die Änderungen werden nach dem nächsten Neustart wirksam.", + "manualRestartRequired": "Nach dem die Plugin-Änderung durchgeführt wurde, muss SCM-Manager neu gestartet werden." } }, "repositoryRole": { diff --git a/scm-ui/ui-webapp/public/locales/en/admin.json b/scm-ui/ui-webapp/public/locales/en/admin.json index 5e7e44dcd2..a244a495b5 100644 --- a/scm-ui/ui-webapp/public/locales/en/admin.json +++ b/scm-ui/ui-webapp/public/locales/en/admin.json @@ -69,7 +69,8 @@ "restartNotification": "You should only restart the scm-manager context if no one else is currently working with it.", "executePending": "The following plugin changes will be executed and after that the scm-manager context will be restarted.", "cancelPending": "The following plugin changes will be canceled.", - "updateAllInfo": "The following plugin changes will be executed. You need to restart the scm-manager to make these changes effective." + "updateAllInfo": "The following plugin changes will be executed. You need to restart the scm-manager to make these changes effective.", + "manualRestartRequired": "After the plugin change has been made, scm-manager must be restarted." } }, "repositoryRole": { diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/PluginModal.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/PluginModal.tsx index 7c1d12a6b2..1074588e59 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/components/PluginModal.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/components/PluginModal.tsx @@ -214,8 +214,25 @@ class PluginModal extends React.Component { }); }; - render() { + createRestartSectionContent = () => { const { restart } = this.state; + const { plugin, pluginAction, t } = this.props; + + if (plugin._links[pluginAction + "WithRestart"]) { + return ( + + ); + } else { + return {t("plugins.modal.manualRestartRequired")}; + } + }; + + render() { const { plugin, pluginAction, onClose, t } = this.props; const body = ( @@ -262,12 +279,7 @@ class PluginModal extends React.Component {

- + {this.createRestartSectionContent()}
{this.renderNotifications()} diff --git a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java index 8304487732..ff2522c352 100644 --- a/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java +++ b/scm-webapp/src/main/java/sonia/scm/api/v2/resources/PluginDtoMapper.java @@ -21,12 +21,13 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import de.otto.edison.hal.Links; import org.mapstruct.Mapper; import org.mapstruct.MappingTarget; +import sonia.scm.lifecycle.Restarter; import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.InstalledPlugin; import sonia.scm.plugin.Plugin; @@ -47,6 +48,9 @@ public abstract class PluginDtoMapper { @Inject private ResourceLinks resourceLinks; + @Inject + private Restarter restarter; + public abstract void map(PluginInformation plugin, @MappingTarget PluginDto dto); public PluginDto mapInstalled(InstalledPlugin plugin, List availablePlugins) { @@ -78,12 +82,20 @@ public abstract class PluginDtoMapper { .self(information.getName())); if (!plugin.isPending() && PluginPermissions.manage().isPermitted()) { - links.single(link("install", resourceLinks.availablePlugin().install(information.getName()))); + String href = resourceLinks.availablePlugin().install(information.getName()); + appendLink(links, "install", href); } return new PluginDto(links.build()); } + private void appendLink(Links.Builder links, String name, String href) { + links.single(link(name, href)); + if (restarter.isSupported()) { + links.single(link(name + "WithRestart", href + "?restart=true")); + } + } + private PluginDto createDtoForInstalled(InstalledPlugin plugin, List availablePlugins) { PluginInformation information = plugin.getDescriptor().getInformation(); Optional availablePlugin = checkForUpdates(plugin, availablePlugins); @@ -96,13 +108,16 @@ public abstract class PluginDtoMapper { && !availablePlugin.get().isPending() && PluginPermissions.manage().isPermitted() ) { - links.single(link("update", resourceLinks.availablePlugin().install(information.getName()))); + String href = resourceLinks.availablePlugin().install(information.getName()); + appendLink(links, "update", href); } + if (plugin.isUninstallable() && (!availablePlugin.isPresent() || !availablePlugin.get().isPending()) && PluginPermissions.manage().isPermitted() ) { - links.single(link("uninstall", resourceLinks.installedPlugin().uninstall(information.getName()))); + String href = resourceLinks.installedPlugin().uninstall(information.getName()); + appendLink(links, "uninstall", href); } PluginDto dto = new PluginDto(links.build()); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapperTest.java index 0029296882..a3fa801a22 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoCollectionMapperTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import de.otto.edison.hal.HalRepresentation; @@ -36,6 +36,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.lifecycle.Restarter; import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.AvailablePluginDescriptor; import sonia.scm.plugin.InstalledPlugin; @@ -59,6 +60,9 @@ class PluginDtoCollectionMapperTest { ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("/")); + @Mock + private Restarter restarter; + @InjectMocks PluginDtoMapperImpl pluginDtoMapper; @@ -142,7 +146,7 @@ class PluginDtoCollectionMapperTest { } @Test - void shouldAddInstallLinkForNewVersionWhenPermitted() { + void shouldAddUpdateLinkForNewVersionWhenPermitted() { when(subject.isPermitted("plugin:manage")).thenReturn(true); PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager); @@ -154,6 +158,21 @@ class PluginDtoCollectionMapperTest { assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty(); } + @Test + void shouldAddUpdateWithRestartLinkForNewVersionWhenPermitted() { + when(restarter.isSupported()).thenReturn(true); + when(subject.isPermitted("plugin:manage")).thenReturn(true); + PluginDtoCollectionMapper mapper = new PluginDtoCollectionMapper(resourceLinks, pluginDtoMapper, manager); + + HalRepresentation result = mapper.mapInstalled( + singletonList(createInstalledPlugin("scm-some-plugin", "1")), + singletonList(createAvailablePlugin("scm-some-plugin", "2"))); + + PluginDto plugin = getPluginDtoFromResult(result); + assertThat(plugin.getLinks().getLinkBy("update")).isNotEmpty(); + assertThat(plugin.getLinks().getLinkBy("updateWithRestart")).isNotEmpty(); + } + @Test void shouldSetInstalledPluginPendingWhenCorrespondingAvailablePluginIsPending() { when(subject.isPermitted("plugin:manage")).thenReturn(true); diff --git a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java index c4ccbe521d..51a7001140 100644 --- a/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java +++ b/scm-webapp/src/test/java/sonia/scm/api/v2/resources/PluginDtoMapperTest.java @@ -21,7 +21,7 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ - + package sonia.scm.api.v2.resources; import com.google.common.collect.ImmutableSet; @@ -35,6 +35,7 @@ import org.mockito.Answers; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import sonia.scm.lifecycle.Restarter; import sonia.scm.plugin.AvailablePlugin; import sonia.scm.plugin.AvailablePluginDescriptor; import sonia.scm.plugin.InstalledPlugin; @@ -56,6 +57,9 @@ class PluginDtoMapperTest { @SuppressWarnings("unused") // Is injected private final ResourceLinks resourceLinks = ResourceLinksMock.createMock(URI.create("https://hitchhiker.com/")); + @Mock + private Restarter restarter; + @InjectMocks private PluginDtoMapperImpl mapper; @@ -122,6 +126,7 @@ class PluginDtoMapperTest { PluginDto dto = mapper.mapAvailable(plugin); assertThat(dto.getLinks().getLinkBy("install")).isEmpty(); + assertThat(dto.getLinks().getLinkBy("installWithRestart")).isEmpty(); } @Test @@ -134,6 +139,17 @@ class PluginDtoMapperTest { .isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/install"); } + @Test + void shouldAppendInstallWithRestartLink() { + when(restarter.isSupported()).thenReturn(true); + when(subject.isPermitted("plugin:manage")).thenReturn(true); + AvailablePlugin plugin = createAvailable(createPluginInformation()); + + PluginDto dto = mapper.mapAvailable(plugin); + assertThat(dto.getLinks().getLinkBy("installWithRestart").get().getHref()) + .isEqualTo("https://hitchhiker.com/v2/plugins/available/scm-cas-plugin/install?restart=true"); + } + @Test void shouldReturnMiscellaneousIfCategoryIsNull() { PluginInformation information = createPluginInformation(); @@ -162,4 +178,17 @@ class PluginDtoMapperTest { assertThat(dto.getLinks().getLinkBy("uninstall").get().getHref()) .isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin/uninstall"); } + + @Test + void shouldAppendUninstallWithRestartLink() { + when(restarter.isSupported()).thenReturn(true); + when(subject.isPermitted("plugin:manage")).thenReturn(true); + + InstalledPlugin plugin = createInstalled(createPluginInformation()); + when(plugin.isUninstallable()).thenReturn(true); + + PluginDto dto = mapper.mapInstalled(plugin, emptyList()); + assertThat(dto.getLinks().getLinkBy("uninstallWithRestart").get().getHref()) + .isEqualTo("https://hitchhiker.com/v2/plugins/installed/scm-cas-plugin/uninstall?restart=true"); + } } From fbc86e7d333790974bcf8c56d98cb46883c560e8 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Tue, 24 Mar 2020 15:58:33 +0100 Subject: [PATCH 07/18] show execute and restart button only if restarting is supported --- scm-ui/ui-webapp/public/locales/de/admin.json | 9 +- scm-ui/ui-webapp/public/locales/en/admin.json | 9 +- .../components/ExecutePendingModal.tsx | 61 +---------- .../components/PendingPluginsQueue.tsx | 66 ++++++++++++ .../plugins/components/ShowPendingModal.tsx | 70 ++++++++++++ .../plugins/containers/PluginsOverview.tsx | 101 ++++++++++++------ .../v2/resources/PendingPluginResource.java | 11 +- .../resources/PendingPluginResourceTest.java | 26 ++++- 8 files changed, 252 insertions(+), 101 deletions(-) create mode 100644 scm-ui/ui-webapp/src/admin/plugins/components/PendingPluginsQueue.tsx create mode 100644 scm-ui/ui-webapp/src/admin/plugins/components/ShowPendingModal.tsx diff --git a/scm-ui/ui-webapp/public/locales/de/admin.json b/scm-ui/ui-webapp/public/locales/de/admin.json index b2ffbfe282..a60b01046c 100644 --- a/scm-ui/ui-webapp/public/locales/de/admin.json +++ b/scm-ui/ui-webapp/public/locales/de/admin.json @@ -30,6 +30,7 @@ "installedNavLink": "Installiert", "availableNavLink": "Verfügbar" }, + "showPending": "Änderungen anzeigen", "executePending": "Änderungen ausführen", "outdatedPlugins": "{{count}} veraltetes Plugin", "outdatedPlugins_plural": "{{count}} veraltete Plugins", @@ -55,6 +56,7 @@ "executeAndRestart": "Ausführen und Neustarten", "updateAll": "Alle Plugins aktualisieren", "abort": "Abbrechen", + "close": "Schließen", "author": "Autor", "version": "Version", "currentVersion": "Installierte Version", @@ -66,11 +68,12 @@ "uninstalledNotification": "Das Plugin wurde erfolgreich installiert. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:", "executedChangesNotification": "Die Plugin Änderungen wurden erfolgreich durchgeführt. Um Änderungen an der UI zu sehen, muss die Seite neu geladen werden:", "reload": "jetzt neu laden", - "restartNotification": "Der SCM-Manager Kontext sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.", - "executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird der SCM-Manager Kontext neu gestartet.", + "restartNotification": "Der SCM-Manager sollte nur neu gestartet werden, wenn aktuell niemand damit arbeitet.", + "executePending": "Die folgenden Plugin-Änderungen werden ausgeführt. Anschließend wird SCM-Manager neu gestartet.", "cancelPending": "Die folgenden Plugin-Änderungen werden abgebrochen und zurückgesetzt.", "updateAllInfo": "Die folgenden Plugins werden aktualisiert. Die Änderungen werden nach dem nächsten Neustart wirksam.", - "manualRestartRequired": "Nach dem die Plugin-Änderung durchgeführt wurde, muss SCM-Manager neu gestartet werden." + "manualRestartRequired": "Nach dem die Plugin-Änderung durchgeführt wurde, muss SCM-Manager neu gestartet werden.", + "showPending": "Um die folgenden Plugin-Änderungen auszuführen muss SCM-Manager neu gestartet werden." } }, "repositoryRole": { diff --git a/scm-ui/ui-webapp/public/locales/en/admin.json b/scm-ui/ui-webapp/public/locales/en/admin.json index a244a495b5..953cf335b4 100644 --- a/scm-ui/ui-webapp/public/locales/en/admin.json +++ b/scm-ui/ui-webapp/public/locales/en/admin.json @@ -30,6 +30,7 @@ "installedNavLink": "Installed", "availableNavLink": "Available" }, + "showPending": "Show changes", "executePending": "Execute changes", "outdatedPlugins": "{{count}} outdated plugin", "outdatedPlugins_plural": "{{count}} outdated plugins", @@ -55,6 +56,7 @@ "executeAndRestart": "Execute and Restart", "updateAll": "Update all plugins", "abort": "Abort", + "close": "Close", "author": "Author", "version": "Version", "currentVersion": "Installed version", @@ -66,11 +68,12 @@ "uninstalledNotification": "Successfully uninstalled plugin. You have to reload the page, to see ui changes:", "executedChangesNotification": "Successfully executed plugin changes. You have to reload the page, to see ui changes:", "reload": "reload now", - "restartNotification": "You should only restart the scm-manager context if no one else is currently working with it.", - "executePending": "The following plugin changes will be executed and after that the scm-manager context will be restarted.", + "restartNotification": "You should only restart scm-manager if no one else is currently working with it.", + "executePending": "The following plugin changes will be executed and after that the scm-manager will be restarted.", "cancelPending": "The following plugin changes will be canceled.", "updateAllInfo": "The following plugin changes will be executed. You need to restart the scm-manager to make these changes effective.", - "manualRestartRequired": "After the plugin change has been made, scm-manager must be restarted." + "manualRestartRequired": "After the plugin change has been made, scm-manager must be restarted.", + "showPending": "To execute the following plugin changes, scm-manager must be restarted." } }, "repositoryRole": { diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/ExecutePendingModal.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/ExecutePendingModal.tsx index c0b35a4027..fbec769aaa 100644 --- a/scm-ui/ui-webapp/src/admin/plugins/components/ExecutePendingModal.tsx +++ b/scm-ui/ui-webapp/src/admin/plugins/components/ExecutePendingModal.tsx @@ -27,6 +27,7 @@ import { PendingPlugins } from "@scm-manager/ui-types"; import { WithTranslation, withTranslation } from "react-i18next"; import waitForRestart from "./waitForRestart"; import SuccessNotification from "./SuccessNotification"; +import PendingPluginsQueue from "./PendingPluginsQueue"; type Props = WithTranslation & { onClose: () => void; @@ -85,70 +86,14 @@ class ExecutePendingModal extends React.Component { }); }; - renderInstallQueue = () => { - const { pendingPlugins, t } = this.props; - return ( - <> - {pendingPlugins._embedded && pendingPlugins._embedded.new.length > 0 && ( - <> - {t("plugins.modal.installQueue")} -
    - {pendingPlugins._embedded.new.map(plugin => ( -
  • {plugin.name}
  • - ))} -
- - )} - - ); - }; - - renderUpdateQueue = () => { - const { pendingPlugins, t } = this.props; - return ( - <> - {pendingPlugins._embedded && pendingPlugins._embedded.update.length > 0 && ( - <> - {t("plugins.modal.updateQueue")} -
    - {pendingPlugins._embedded.update.map(plugin => ( -
  • {plugin.name}
  • - ))} -
- - )} - - ); - }; - - renderUninstallQueue = () => { - const { pendingPlugins, t } = this.props; - return ( - <> - {pendingPlugins._embedded && pendingPlugins._embedded.uninstall.length > 0 && ( - <> - {t("plugins.modal.uninstallQueue")} -
    - {pendingPlugins._embedded.uninstall.map(plugin => ( -
  • {plugin.name}
  • - ))} -
- - )} - - ); - }; - renderBody = () => { - const { t } = this.props; + const { pendingPlugins, t } = this.props; return ( <>

{t("plugins.modal.executePending")}

- {this.renderInstallQueue()} - {this.renderUpdateQueue()} - {this.renderUninstallQueue()} +
{this.renderNotifications()}
diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/PendingPluginsQueue.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/PendingPluginsQueue.tsx new file mode 100644 index 0000000000..f8fd298674 --- /dev/null +++ b/scm-ui/ui-webapp/src/admin/plugins/components/PendingPluginsQueue.tsx @@ -0,0 +1,66 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +import React, { FC } from "react"; +import { PendingPlugins } from "@scm-manager/ui-types"; +import { useTranslation } from "react-i18next"; + +type Props = { + pendingPlugins: PendingPlugins; +}; + +type SectionProps = Props & { + type: string; + label: string; +}; + +const Section: FC = ({ pendingPlugins, type, label }) => { + const plugins = pendingPlugins?._embedded[type]; + if (!plugins || plugins.length === 0) { + return null; + } + return ( + <> + {label} +
    + {plugins.map(plugin => ( +
  • {plugin.name}
  • + ))} +
+ + ); +}; + +const PendingPluginsQueue: FC = ({ pendingPlugins }) => { + const [t] = useTranslation("admin"); + return ( + <> +
+
+
+ + ); +}; + +export default PendingPluginsQueue; diff --git a/scm-ui/ui-webapp/src/admin/plugins/components/ShowPendingModal.tsx b/scm-ui/ui-webapp/src/admin/plugins/components/ShowPendingModal.tsx new file mode 100644 index 0000000000..d28b51f927 --- /dev/null +++ b/scm-ui/ui-webapp/src/admin/plugins/components/ShowPendingModal.tsx @@ -0,0 +1,70 @@ +/* + * MIT License + * + * Copyright (c) 2020-present Cloudogu GmbH and Contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +import React, { FC } from "react"; +import { Button, Modal, Notification } from "@scm-manager/ui-components"; +import { PendingPlugins } from "@scm-manager/ui-types"; +import { useTranslation } from "react-i18next"; +import PendingPluginsQueue from "./PendingPluginsQueue"; + + +type ModalBodyProps = { + pendingPlugins: PendingPlugins; +}; + +const ModalBody: FC = ({ pendingPlugins }) => { + const [t] = useTranslation("admin"); + return ( + <> +
+
+

{t("plugins.modal.showPending")}

+ +
+
+
+ {t("plugins.modal.restartNotification")} +
+ + ); +}; + +type Props = { + onClose: () => void; + pendingPlugins: PendingPlugins; +}; + +const ShowPendingModal: FC = ({ pendingPlugins, onClose }) => { + const [t] = useTranslation("admin"); + return ( + } + footer={