From bac253d276778078a7423640423aaf607be06d21 Mon Sep 17 00:00:00 2001 From: Sebastian Sdorra Date: Sun, 3 May 2020 11:23:42 +0200 Subject: [PATCH] implemented restart strategy for windows services --- .../scm/lifecycle/RestartStrategyFactory.java | 40 +++-- .../scm/lifecycle/WinSWRestartStrategy.java | 49 +++++- .../lifecycle/RestartStrategyFactoryTest.java | 163 +++++++++++++++++- .../scm/lifecycle/RestartStrategyTest.java | 139 --------------- 4 files changed, 240 insertions(+), 151 deletions(-) delete mode 100644 scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java 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 4520bb3ddf..e4a79ae1ef 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/RestartStrategyFactory.java @@ -23,11 +23,14 @@ */ package sonia.scm.lifecycle; +import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Strings; -import sonia.scm.PlatformType; +import sonia.scm.Platform; import sonia.scm.util.SystemUtil; import java.lang.reflect.Constructor; +import java.util.Map; +import java.util.Properties; final class RestartStrategyFactory { @@ -41,7 +44,15 @@ final class RestartStrategyFactory { */ static final String STRATEGY_NONE = "none"; - private RestartStrategyFactory() { + private final Platform platform; + private final Map environment; + private final Properties systemProperties; + + @VisibleForTesting + RestartStrategyFactory(Platform platform, Map environment, Properties systemProperties) { + this.platform = platform; + this.environment = environment; + this.systemProperties = systemProperties; } /** @@ -51,14 +62,24 @@ final class RestartStrategyFactory { * @return configured strategy or {@code null} */ static RestartStrategy create(ClassLoader webAppClassLoader) { - String property = System.getProperty(PROPERTY_STRATEGY); + RestartStrategyFactory factory = new RestartStrategyFactory( + SystemUtil.getPlatform(), + System.getenv(), + System.getProperties() + ); + return factory.fromClassLoader(webAppClassLoader); + } + + @VisibleForTesting + RestartStrategy fromClassLoader(ClassLoader webAppClassLoader) { + String property = systemProperties.getProperty(PROPERTY_STRATEGY); if (Strings.isNullOrEmpty(property)) { return forPlatform(); } return fromProperty(webAppClassLoader, property); } - private static RestartStrategy fromProperty(ClassLoader webAppClassLoader, String property) { + private RestartStrategy fromProperty(ClassLoader webAppClassLoader, String property) { if (STRATEGY_NONE.equalsIgnoreCase(property)) { return null; } else if (ExitRestartStrategy.NAME.equalsIgnoreCase(property)) { @@ -68,7 +89,7 @@ final class RestartStrategyFactory { } } - private static RestartStrategy fromClassName(String className, ClassLoader classLoader) { + private RestartStrategy fromClassName(String className, ClassLoader classLoader) { try { Class rsClass = Class.forName(className).asSubclass(RestartStrategy.class); return createInstance(rsClass, classLoader); @@ -77,7 +98,7 @@ final class RestartStrategyFactory { } } - private static RestartStrategy createInstance(Class rsClass, ClassLoader classLoader) throws InstantiationException, IllegalAccessException, java.lang.reflect.InvocationTargetException, NoSuchMethodException { + private RestartStrategy createInstance(Class rsClass, ClassLoader classLoader) throws InstantiationException, IllegalAccessException, java.lang.reflect.InvocationTargetException, NoSuchMethodException { try { Constructor constructor = rsClass.getConstructor(ClassLoader.class); return constructor.newInstance(classLoader); @@ -86,12 +107,11 @@ final class RestartStrategyFactory { } } - 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); + private RestartStrategy forPlatform() { if (platform.isPosix()) { return new PosixRestartStrategy(); + } else if (platform.isWindows() && WinSWRestartStrategy.isSupported(environment)) { + return new WinSWRestartStrategy(); } return null; } diff --git a/scm-webapp/src/main/java/sonia/scm/lifecycle/WinSWRestartStrategy.java b/scm-webapp/src/main/java/sonia/scm/lifecycle/WinSWRestartStrategy.java index ea66623e5c..b6f3a3d95a 100644 --- a/scm-webapp/src/main/java/sonia/scm/lifecycle/WinSWRestartStrategy.java +++ b/scm-webapp/src/main/java/sonia/scm/lifecycle/WinSWRestartStrategy.java @@ -1,4 +1,51 @@ package sonia.scm.lifecycle; -public class WinSWRestartStrategy { +import com.google.common.base.Strings; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.util.Map; + +/** + * A {@link RestartStrategy} which can be used if scm-manager was started as windows + * service with WinSW. + * + * @see Self-restarting Windows services + */ +class WinSWRestartStrategy extends RestartStrategy { + + private static final Logger LOG = LoggerFactory.getLogger(WinSWRestartStrategy.class); + + static final String ENV_EXECUTABLE = "WINSW_EXECUTABLE"; + + @Override + @SuppressWarnings("java:S2142") + protected void executeRestart(InjectionContext context) { + String executablePath = System.getenv(ENV_EXECUTABLE); + try { + int rs = execute(executablePath); + if (rs != 0) { + LOG.error("winsw {} returned status code {}", executablePath, rs); + } + } catch (IOException | InterruptedException e) { + LOG.error("failed to execute winsw at {}", executablePath, e); + } + LOG.error("scm-manager is in an unrecoverable state, we will now exit the java process"); + System.exit(1); + } + + private int execute(String executablePath) throws InterruptedException, IOException { + return new ProcessBuilder(executablePath, "restart!").start().waitFor(); + } + + static boolean isSupported(Map environment) { + String executablePath = environment.get(ENV_EXECUTABLE); + if (Strings.isNullOrEmpty(executablePath)) { + return false; + } + File exe = new File(executablePath); + return exe.exists(); + } } diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyFactoryTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyFactoryTest.java index ad7e293321..9178286282 100644 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyFactoryTest.java +++ b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyFactoryTest.java @@ -1,5 +1,166 @@ -import static org.junit.jupiter.api.Assertions.*; +/* + * 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.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.junitpioneer.jupiter.TempDirectory; +import sonia.scm.Platform; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@ExtendWith(TempDirectory.class) class RestartStrategyFactoryTest { + private final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + + @Test + void shouldReturnRestartStrategyFromSystemProperty() { + RestartStrategyFactory factory = builder().withStrategy(TestingRestartStrategy.class).create(); + RestartStrategy restartStrategy = factory.fromClassLoader(classLoader); + assertThat(restartStrategy).isInstanceOf(TestingRestartStrategy.class); + } + + @Test + void shouldReturnRestartStrategyFromSystemPropertyWithClassLoaderConstructor() { + RestartStrategyFactory factory = builder().withStrategy(ComplexRestartStrategy.class).create(); + RestartStrategy restartStrategy = factory.fromClassLoader(classLoader); + assertThat(restartStrategy).isInstanceOf(ComplexRestartStrategy.class) + .extracting("classLoader") + .isSameAs(classLoader); + } + + @Test + void shouldThrowExceptionForNonStrategyClass() { + RestartStrategyFactory factory = builder().withStrategy(RestartStrategyFactoryTest.class).create(); + assertThrows(RestartNotSupportedException.class, () -> factory.fromClassLoader(classLoader)); + } + + @Test + void shouldReturnEmpty() { + RestartStrategyFactory factory = builder().withStrategy(RestartStrategyFactory.STRATEGY_NONE).create(); + assertThat(factory.fromClassLoader(classLoader)).isNull(); + } + + @Test + void shouldReturnEmptyForUnknownOs() { + RestartStrategyFactory factory = builder().withOs("hitchhiker-os").create(); + assertThat(factory.fromClassLoader(classLoader)).isNull(); + } + + @Test + void shouldReturnExitRestartStrategy() { + RestartStrategyFactory factory = builder().withStrategy(ExitRestartStrategy.NAME).create(); + assertThat(factory.fromClassLoader(classLoader)).isInstanceOf(ExitRestartStrategy.class); + } + + @ParameterizedTest + @ValueSource(strings = { "linux", "darwin", "solaris", "freebsd", "openbsd" }) + void shouldReturnPosixRestartStrategyForPosixBased(String os) { + RestartStrategyFactory factory = builder().withOs(os).create(); + assertThat(factory.fromClassLoader(classLoader)).isInstanceOf(PosixRestartStrategy.class); + } + + @Test + void shouldReturnWinSWRestartStrategy(@TempDirectory.TempDir Path tempDir) throws IOException { + File exe = tempDir.resolve("winsw.exe").toFile(); + exe.createNewFile(); + + RestartStrategyFactory factory = builder() + .withOs("windows") + .withEnvironment(WinSWRestartStrategy.ENV_EXECUTABLE, exe.getAbsolutePath()) + .create(); + assertThat(factory.fromClassLoader(classLoader)).isInstanceOf(WinSWRestartStrategy.class); + } + + public static class TestingRestartStrategy extends RestartStrategy { + @Override + protected void executeRestart(InjectionContext context) { + } + } + + public static class ComplexRestartStrategy extends RestartStrategy { + + private final ClassLoader classLoader; + + public ComplexRestartStrategy(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + @Override + protected void executeRestart(InjectionContext context) { + } + } + + private static Builder builder() { + return new Builder(); + } + + private static class Builder { + + private final Properties properties = new Properties(); + private final Map environment = new HashMap<>(); + private Platform platform = new Platform("Linux", "64Bit", "x64"); + + public Builder withStrategy(Class strategy) { + return withStrategy(strategy.getName()); + } + + public Builder withStrategy(String strategy) { + return withProperty(RestartStrategyFactory.PROPERTY_STRATEGY, strategy); + } + + public Builder withProperty(String key, String value) { + properties.setProperty(key, value); + return this; + } + + public Builder withEnvironment(String key, String value) { + environment.put(key, value); + return this; + } + + public Builder withOs(String os) { + platform = new Platform(os, "64Bit", "x64"); + return this; + } + + public RestartStrategyFactory create() { + return new RestartStrategyFactory(platform, environment, properties); + } + + } + } diff --git a/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java b/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java deleted file mode 100644 index 2205bfc0d0..0000000000 --- a/scm-webapp/src/test/java/sonia/scm/lifecycle/RestartStrategyTest.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * 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; -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 shouldReturnRestartStrategyFromSystemPropertyWithClassLoaderConstructor() { - withStrategy(ComplexRestartStrategy.class.getName(), (rs) -> { - assertThat(rs).containsInstanceOf(ComplexRestartStrategy.class) - .get() - .extracting("classLoader") - .isSameAs(classLoader); - }); - } - - @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); - }); - } - - @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 extends RestartStrategy { - @Override - protected void executeRestart(InjectionContext context) { - } - } - - public static class ComplexRestartStrategy extends RestartStrategy { - - private final ClassLoader classLoader; - - public ComplexRestartStrategy(ClassLoader classLoader) { - this.classLoader = classLoader; - } - - @Override - protected void executeRestart(InjectionContext context) { - } - } - -}