<modules>
<module>sonar-process</module>
- <module>sonar-process-monitor</module>
+ <module>sonar-main</module>
<module>sonar-db-core</module>
<module>sonar-db-migration</module>
<module>sonar-db-dao</module>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.sonarsource.sonarqube</groupId>
+ <artifactId>server</artifactId>
+ <version>6.6-SNAPSHOT</version>
+ <relativePath>../</relativePath>
+ </parent>
+
+ <artifactId>sonar-main</artifactId>
+ <name>SonarQube :: Main Process</name>
+
+ <properties>
+ <!--
+ version as stored in JAR and displayed in webapp. It is
+ overridden on Travis when replacing SNAPSHOT version by
+ build unique version, for instance "6.3.0.12345".
+ -->
+ <buildVersion>${project.version}</buildVersion>
+ </properties>
+
+ <dependencies>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>sonar-process</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-to-slf4j</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.logging.log4j</groupId>
+ <artifactId>log4j-core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.hazelcast</groupId>
+ <artifactId>hazelcast</artifactId>
+ </dependency>
+ <!--
+ Required by our usage of Guava for clustering : CeWorkerFactoryImpl.getClusteredWorkerUUIDs()
+ -->
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.elasticsearch.client</groupId>
+ <artifactId>transport</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.hazelcast</groupId>
+ <artifactId>hazelcast-client</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <resources>
+ <resource>
+ <!-- Used to resolve variables in file sonarqube-version.txt -->
+ <directory>src/main/resources</directory>
+ <filtering>true</filtering>
+ </resource>
+ </resources>
+ </build>
+</project>
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.FileVisitOption;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.util.EnumSet;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.application.config.AppSettings;
+import org.sonar.process.sharedmemoryfile.AllProcessesCommands;
+
+import static java.lang.String.format;
+import static java.nio.file.FileVisitResult.CONTINUE;
+import static org.apache.commons.io.FileUtils.forceMkdir;
+import static org.sonar.process.FileUtils2.deleteDirectory;
+import static org.sonar.process.ProcessProperties.PATH_DATA;
+import static org.sonar.process.ProcessProperties.PATH_LOGS;
+import static org.sonar.process.ProcessProperties.PATH_TEMP;
+import static org.sonar.process.ProcessProperties.PATH_WEB;
+
+public class AppFileSystem implements FileSystem {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AppFileSystem.class);
+ private static final EnumSet<FileVisitOption> FOLLOW_LINKS = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
+
+ private final AppSettings settings;
+
+ public AppFileSystem(AppSettings settings) {
+ this.settings = settings;
+ }
+
+ @Override
+ public void reset() throws IOException {
+ createDirectory(PATH_DATA);
+ createDirectory(PATH_WEB);
+ createDirectory(PATH_LOGS);
+ File tempDir = createOrCleanTempDirectory(PATH_TEMP);
+ try (AllProcessesCommands allProcessesCommands = new AllProcessesCommands(tempDir)) {
+ allProcessesCommands.clean();
+ }
+ }
+
+ @Override
+ public File getTempDir() {
+ return settings.getProps().nonNullValueAsFile(PATH_TEMP);
+ }
+
+ private boolean createDirectory(String propKey) throws IOException {
+ File dir = settings.getProps().nonNullValueAsFile(propKey);
+ if (dir.exists()) {
+ ensureIsNotAFile(propKey, dir);
+ return false;
+ }
+
+ forceMkdir(dir);
+ ensureIsNotAFile(propKey, dir);
+ return true;
+ }
+
+ private static void ensureIsNotAFile(String propKey, File dir) {
+ if (!dir.isDirectory()) {
+ throw new IllegalStateException(format("Property '%s' is not valid, not a directory: %s",
+ propKey, dir.getAbsolutePath()));
+ }
+ }
+
+ private File createOrCleanTempDirectory(String propKey) throws IOException {
+ File dir = settings.getProps().nonNullValueAsFile(propKey);
+ LOG.info("Cleaning or creating temp directory {}", dir.getAbsolutePath());
+ if (!createDirectory(propKey)) {
+ Files.walkFileTree(dir.toPath(), FOLLOW_LINKS, CleanTempDirFileVisitor.VISIT_MAX_DEPTH, new CleanTempDirFileVisitor(dir.toPath()));
+ }
+ return dir;
+ }
+
+ private static class CleanTempDirFileVisitor extends SimpleFileVisitor<Path> {
+ private static final Path SHAREDMEMORY_FILE = Paths.get("sharedmemory");
+ static final int VISIT_MAX_DEPTH = 1;
+
+ private final Path path;
+ private final boolean symLink;
+
+ CleanTempDirFileVisitor(Path path) {
+ this.path = path;
+ this.symLink = Files.isSymbolicLink(path);
+ }
+
+ @Override
+ public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs) throws IOException {
+ File file = filePath.toFile();
+ if (file.isDirectory()) {
+ deleteDirectory(file);
+ } else if (filePath.getFileName().equals(SHAREDMEMORY_FILE)) {
+ return CONTINUE;
+ } else if (!symLink || !filePath.equals(path)) {
+ Files.delete(filePath);
+ }
+ return CONTINUE;
+ }
+
+ @Override
+ public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
+ if (!dir.equals(path)) {
+ deleteDirectory(dir.toFile());
+ }
+ return CONTINUE;
+ }
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.ConsoleAppender;
+import ch.qos.logback.core.FileAppender;
+import org.sonar.application.config.AppSettings;
+import org.sonar.application.process.StreamGobbler;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.logging.LogLevelConfig;
+import org.sonar.process.logging.LogbackHelper;
+import org.sonar.process.logging.RootLoggerConfig;
+
+import static org.slf4j.Logger.ROOT_LOGGER_NAME;
+import static org.sonar.application.process.StreamGobbler.LOGGER_GOBBLER;
+import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder;
+
+/**
+ * Configure logback for the APP process.
+ *
+ * <p>
+ * SonarQube's logging use cases:
+ * <ol>
+ * <li>
+ * SQ started as a background process (with {@code sonar.sh start}):
+ * <ul>
+ * <li>
+ * logs produced by the JVM before logback is setup in the APP JVM or which can't be caught by logback
+ * (such as JVM crash) must be written to sonar.log
+ * </li>
+ * <li>
+ * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught
+ * by logback (such as JVM crash) must be written to sonar.log
+ * </li>
+ * <li>each JVM writes its own logs into its dedicated file</li>
+ * </ul>
+ * </li>
+ * <li>
+ * SQ started in console with wrapper (ie. with {@code sonar.sh console}):
+ * <ul>
+ * <li>
+ * logs produced by the APP JVM before logback is setup in the APP JVM or which can't be caught by logback
+ * (such as JVM crash) must be written to sonar.log
+ * </li>
+ * <li>
+ * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught
+ * by logback (such as JVM crash) must be written to sonar.log
+ * </li>
+ * <li>each JVM writes its own logs into its dedicated file</li>
+ * <li>APP JVM logs are written to the APP JVM {@code System.out}</li>
+ * </ul>
+ * </li>
+ * <li>
+ * SQ started from command line (ie. {@code java -jar sonar-application-X.Y.jar}):
+ * <ul>
+ * <li>
+ * logs produced by the APP JVM before logback is setup in the APP JVM or which can't be caught by logback
+ * (such as JVM crash) are the responsibility of the user to be dealt with
+ * </li>
+ * <li>
+ * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught
+ * by logback (such as JVM crash) must be written to APP's {@code System.out}
+ * </li>
+ * <li>each JVM writes its own logs into its dedicated file</li>
+ * <li>APP JVM logs are written to the APP JVM {@code System.out}</li>
+ * </ul>
+ * </li>
+ * <li>
+ * SQ started from an IT (ie. from command line with {@code option -Dsonar.log.console=true}):
+ * <ul>
+ * <li>
+ * logs produced by the APP JVM before logback is setup in the APP JVM or which can't be caught by logback
+ * (such as JVM crash) are the responsibility of the developer or maven to be dealt with
+ * </li>
+ * <li>
+ * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught
+ * by logback (such as JVM crash) must be written to APP's {@code System.out} and are the responsibility of the
+ * developer or maven to be dealt with
+ * </li>
+ * <li>each JVM writes its own logs into its dedicated file</li>
+ * <li>logs of all 4 JVMs are also written to the APP JVM {@code System.out}</li>
+ * </ul>
+ * </li>
+ * </ol>
+ * </p>
+ *
+ */
+public class AppLogging {
+
+ private static final String CONSOLE_LOGGER = "console";
+ private static final String CONSOLE_PLAIN_APPENDER = "CONSOLE";
+ private static final String APP_CONSOLE_APPENDER = "APP_CONSOLE";
+ private static final String GOBBLER_PLAIN_CONSOLE = "GOBBLER_CONSOLE";
+ private static final RootLoggerConfig APP_ROOT_LOGGER_CONFIG = newRootLoggerConfigBuilder()
+ .setProcessId(ProcessId.APP)
+ .build();
+
+ private final LogbackHelper helper = new LogbackHelper();
+ private final AppSettings appSettings;
+
+ public AppLogging(AppSettings appSettings) {
+ this.appSettings = appSettings;
+ }
+
+ public LoggerContext configure() {
+ LoggerContext ctx = helper.getRootContext();
+ ctx.reset();
+
+ helper.enableJulChangePropagation(ctx);
+
+ configureConsole(ctx);
+ if (helper.isAllLogsToConsoleEnabled(appSettings.getProps()) || !appSettings.getProps().valueAsBoolean("sonar.wrapped", false)) {
+ configureWithLogbackWritingToFile(ctx);
+ } else {
+ configureWithWrapperWritingToFile(ctx);
+ }
+ helper.apply(
+ LogLevelConfig.newBuilder(helper.getRootLoggerName())
+ .rootLevelFor(ProcessId.APP)
+ .immutableLevel("com.hazelcast",
+ Level.toLevel(
+ appSettings.getProps().nonNullValue(ProcessProperties.HAZELCAST_LOG_LEVEL)))
+ .build(),
+ appSettings.getProps());
+
+ return ctx;
+ }
+
+ /**
+ * Creates a non additive logger dedicated to printing message as is (ie. assuming they are already formatted).
+ *
+ * It creates a dedicated appender to the System.out which applies no formatting the logs it receives.
+ */
+ private void configureConsole(LoggerContext loggerContext) {
+ ConsoleAppender<ILoggingEvent> consoleAppender = helper.newConsoleAppender(loggerContext, CONSOLE_PLAIN_APPENDER, "%msg%n");
+
+ Logger consoleLogger = loggerContext.getLogger(CONSOLE_LOGGER);
+ consoleLogger.setAdditive(false);
+ consoleLogger.addAppender(consoleAppender);
+ }
+
+ /**
+ * The process has been started by orchestrator (ie. via {@code java -jar} and optionally passing the option {@code -Dsonar.log.console=true}).
+ * Therefor, APP's System.out (and System.err) are <strong>not</strong> copied to sonar.log by the wrapper and
+ * printing to sonar.log must be done at logback level.
+ */
+ private void configureWithLogbackWritingToFile(LoggerContext ctx) {
+ // configure all logs (ie. root logger) to be written to sonar.log and also to the console with formatting
+ // in practice, this will be only APP's own logs as logs from sub processes LOGGER_GOBBLER and LOGGER_GOBBLER
+ // is configured below to be detached from root
+ // so, this will make all APP's log to be both written to sonar.log and visible in the console
+ configureRootWithLogbackWritingToFile(ctx);
+
+ // if option -Dsonar.log.console=true has been set, sub processes will write their logs to their own files but also
+ // copy them to their System.out.
+ // otherwise, the only logs to be expected in LOGGER_GOBBLER are those before logback is setup in subprocesses or
+ // when their JVM crashes
+ // they must be printed to App's System.out as is (as they are already formatted)
+ // logger is configured to be non additive as we don't want these logs to be written to sonar.log and duplicated in
+ // the console (with an incorrect formatting)
+ configureGobbler(ctx);
+ }
+
+ /**
+ * SQ has been started by the wrapper (ie. with sonar.sh) therefor, APP's System.out (and System.err) are written to
+ * sonar.log by the wrapper.
+ */
+ private void configureWithWrapperWritingToFile(LoggerContext ctx) {
+ // configure all logs (ie. root logger) to be written to console with formatting
+ // in practice, this will be only APP's own logs as logs from sub processes are written to LOGGER_GOBBLER and
+ // LOGGER_GOBBLER is configured below to be detached from root
+ // logs are written to the console because we want them to be in sonar.log and the wrapper will write any log
+ // from APP's System.out and System.err to sonar.log
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ rootLogger.addAppender(createAppConsoleAppender(ctx, helper.buildLogPattern(APP_ROOT_LOGGER_CONFIG)));
+
+ // in regular configuration, sub processes are not copying their logs to their System.out, so, the only logs to be
+ // expected in LOGGER_GOBBLER are those before logback is setup in subprocesses or when JVM crashes
+ // so, they must be printed to App's System.out as is (as they are already formatted) and the wrapper will write
+ // them to sonar.log
+ // logger is configured to be non additive as we don't want these logs written to sonar.log and duplicated in the
+ // console with an incorrect formatting
+ configureGobbler(ctx);
+ }
+
+ private void configureRootWithLogbackWritingToFile(LoggerContext ctx) {
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ String appLogPattern = helper.buildLogPattern(APP_ROOT_LOGGER_CONFIG);
+ FileAppender<ILoggingEvent> fileAppender = helper.newFileAppender(ctx, appSettings.getProps(), APP_ROOT_LOGGER_CONFIG, appLogPattern);
+ rootLogger.addAppender(fileAppender);
+ rootLogger.addAppender(createAppConsoleAppender(ctx, appLogPattern));
+ }
+
+ /**
+ * Configure the logger to which logs from sub processes are written to
+ * (called {@link StreamGobbler#LOGGER_GOBBLER}) by {@link StreamGobbler},
+ * to be:
+ * <ol>
+ * <li>non additive (ie. these logs will be output by the appender of {@link StreamGobbler#LOGGER_GOBBLER} and only this one)</li>
+ * <li>write logs as is (ie. without any extra formatting)</li>
+ * <li>write exclusively to App's System.out</li>
+ * </ol>
+ */
+ private void configureGobbler(LoggerContext ctx) {
+ Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
+ gobblerLogger.setAdditive(false);
+ gobblerLogger.addAppender(helper.newConsoleAppender(ctx, GOBBLER_PLAIN_CONSOLE, "%msg%n"));
+ }
+
+ private ConsoleAppender<ILoggingEvent> createAppConsoleAppender(LoggerContext ctx, String appLogPattern) {
+ return helper.newConsoleAppender(ctx, APP_CONSOLE_APPENDER, appLogPattern);
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.io.IOException;
+import org.sonar.application.config.AppSettings;
+
+/**
+ * Reload settings, reset logging and file system when a
+ * server restart has been requested.
+ */
+public interface AppReloader {
+
+ /**
+ * This method is called when server is down.
+ */
+ void reload(AppSettings settings) throws IOException;
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.io.IOException;
+import java.util.Objects;
+import org.sonar.application.config.AppSettings;
+import org.sonar.application.config.AppSettingsLoader;
+import org.sonar.application.config.ClusterSettings;
+import org.sonar.process.MessageException;
+import org.sonar.process.Props;
+
+import static java.lang.String.format;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.PATH_DATA;
+import static org.sonar.process.ProcessProperties.PATH_LOGS;
+import static org.sonar.process.ProcessProperties.PATH_TEMP;
+import static org.sonar.process.ProcessProperties.PATH_WEB;
+
+public class AppReloaderImpl implements AppReloader {
+
+ private final AppSettingsLoader settingsLoader;
+ private final FileSystem fileSystem;
+ private final AppState appState;
+ private final AppLogging logging;
+
+ public AppReloaderImpl(AppSettingsLoader settingsLoader, FileSystem fileSystem, AppState appState, AppLogging logging) {
+ this.settingsLoader = settingsLoader;
+ this.fileSystem = fileSystem;
+ this.appState = appState;
+ this.logging = logging;
+ }
+
+ @Override
+ public void reload(AppSettings settings) throws IOException {
+ if (ClusterSettings.isClusterEnabled(settings)) {
+ throw new IllegalStateException("Restart is not possible with cluster mode");
+ }
+ AppSettings reloaded = settingsLoader.load();
+ ensureUnchangedConfiguration(settings.getProps(), reloaded.getProps());
+ settings.reload(reloaded.getProps());
+
+ fileSystem.reset();
+ logging.configure();
+ appState.reset();
+ }
+
+ private static void ensureUnchangedConfiguration(Props oldProps, Props newProps) {
+ verifyUnchanged(oldProps, newProps, PATH_DATA);
+ verifyUnchanged(oldProps, newProps, PATH_WEB);
+ verifyUnchanged(oldProps, newProps, PATH_LOGS);
+ verifyUnchanged(oldProps, newProps, PATH_TEMP);
+ verifyUnchanged(oldProps, newProps, CLUSTER_ENABLED);
+ }
+
+ private static void verifyUnchanged(Props initialProps, Props newProps, String propKey) {
+ String initialValue = initialProps.nonNullValue(propKey);
+ String newValue = newProps.nonNullValue(propKey);
+ if (!Objects.equals(initialValue, newValue)) {
+ throw new MessageException(format("Property [%s] cannot be changed on restart: [%s] => [%s]", propKey, initialValue, newValue));
+ }
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.util.Optional;
+import org.sonar.process.ProcessId;
+
+public interface AppState extends AutoCloseable {
+
+ void addListener(AppStateListener listener);
+
+ /**
+ * Whether the process with the specified {@code processId}
+ * has been marked as operational.
+ *
+ * If parameter {@code local} is {@code true}, then only the
+ * process on the local node is requested.
+ *
+ * If parameter {@code local} is {@code false}, then only
+ * the processes on remote nodes are requested, excluding
+ * the local node. In this case at least one process must
+ * be marked as operational.
+ */
+ boolean isOperational(ProcessId processId, boolean local);
+
+ /**
+ * Mark local process as operational. In cluster mode, this
+ * event is propagated to all nodes.
+ */
+ void setOperational(ProcessId processId);
+
+ boolean tryToLockWebLeader();
+
+ void reset();
+
+ void registerSonarQubeVersion(String sonarqubeVersion);
+
+ Optional<String> getLeaderHostName();
+
+ @Override
+ void close();
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import org.sonar.application.cluster.AppStateClusterImpl;
+import org.sonar.application.config.AppSettings;
+import org.sonar.application.config.ClusterSettings;
+
+public class AppStateFactory {
+
+ private final AppSettings settings;
+
+ public AppStateFactory(AppSettings settings) {
+ this.settings = settings;
+ }
+
+ public AppState create() {
+ return ClusterSettings.isClusterEnabled(settings) ? new AppStateClusterImpl(settings) : new AppStateImpl();
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nonnull;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessId;
+
+public class AppStateImpl implements AppState {
+
+ private final Map<ProcessId, Boolean> processes = new EnumMap<>(ProcessId.class);
+ private final List<AppStateListener> listeners = new ArrayList<>();
+ private final AtomicBoolean webLeaderLocked = new AtomicBoolean(false);
+
+ @Override
+ public void addListener(@Nonnull AppStateListener listener) {
+ this.listeners.add(listener);
+ }
+
+ @Override
+ public boolean isOperational(ProcessId processId, boolean local) {
+ return processes.computeIfAbsent(processId, p -> false);
+ }
+
+ @Override
+ public void setOperational(ProcessId processId) {
+ processes.put(processId, true);
+ listeners.forEach(l -> l.onAppStateOperational(processId));
+ }
+
+ @Override
+ public boolean tryToLockWebLeader() {
+ return webLeaderLocked.compareAndSet(false, true);
+ }
+
+ @Override
+ public void reset() {
+ webLeaderLocked.set(false);
+ processes.clear();
+ }
+
+ @Override
+ public void registerSonarQubeVersion(String sonarqubeVersion) {
+ // Nothing to do on non clustered version
+ }
+
+ @Override
+ public Optional<String> getLeaderHostName() {
+ return Optional.of(NetworkUtils.getHostName());
+ }
+
+ @Override
+ public void close() {
+ // nothing to do
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import org.sonar.process.ProcessId;
+
+@FunctionalInterface
+public interface AppStateListener {
+
+ /**
+ * The method is called when the state is changed. When cluster
+ * mode is enabled, the event may be raised from another node.
+ *
+ * Listener must subscribe to {@link AppState#addListener(AppStateListener)}.
+ */
+ void onAppStateOperational(ProcessId processId);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.io.File;
+import java.io.IOException;
+
+public interface FileSystem {
+
+ void reset() throws IOException;
+
+ File getTempDir();
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.sonar.application.NodeLifecycle.State.INIT;
+import static org.sonar.application.NodeLifecycle.State.OPERATIONAL;
+import static org.sonar.application.NodeLifecycle.State.STARTING;
+import static org.sonar.application.NodeLifecycle.State.STOPPED;
+import static org.sonar.application.NodeLifecycle.State.STOPPING;
+
+/**
+ * Lifecycle of the cluster node, consolidating the states
+ * of child processes.
+ */
+class NodeLifecycle {
+ private static final Logger LOG = LoggerFactory.getLogger(NodeLifecycle.class);
+
+ enum State {
+ // initial state, does nothing
+ INIT,
+
+ // at least one process is still starting
+ STARTING,
+
+ // all the processes are started and operational
+ OPERATIONAL,
+
+ // at least one process is still stopping
+ STOPPING,
+
+ // all processes are stopped
+ STOPPED
+ }
+
+ private static final Map<State, Set<State>> TRANSITIONS = buildTransitions();
+
+ private State state = INIT;
+
+ private static Map<State, Set<State>> buildTransitions() {
+ Map<State, Set<State>> res = new EnumMap<>(State.class);
+ res.put(INIT, toSet(STARTING));
+ res.put(STARTING, toSet(OPERATIONAL, STOPPING, STOPPED));
+ res.put(OPERATIONAL, toSet(STOPPING, STOPPED));
+ res.put(STOPPING, toSet(STOPPED));
+ res.put(STOPPED, toSet(STARTING));
+ return res;
+ }
+
+ private static Set<State> toSet(State... states) {
+ if (states.length == 0) {
+ return Collections.emptySet();
+ }
+ if (states.length == 1) {
+ return Collections.singleton(states[0]);
+ }
+ return EnumSet.copyOf(Arrays.asList(states));
+ }
+
+ State getState() {
+ return state;
+ }
+
+ synchronized boolean tryToMoveTo(State to) {
+ boolean res = false;
+ State currentState = state;
+ if (TRANSITIONS.get(currentState).contains(to)) {
+ this.state = to;
+ res = true;
+ }
+ LOG.trace("tryToMoveTo from {} to {} => {}", currentState, to, res);
+ return res;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+public interface Scheduler {
+
+ void schedule();
+
+ /**
+ * Stops all processes and waits for them to be down.
+ */
+ void terminate();
+
+ /**
+ * Blocks until all processes are down
+ */
+ void awaitTermination();
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.util.EnumMap;
+import java.util.Optional;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.application.config.AppSettings;
+import org.sonar.application.config.ClusterSettings;
+import org.sonar.process.command.CommandFactory;
+import org.sonar.process.command.EsCommand;
+import org.sonar.process.command.JavaCommand;
+import org.sonar.application.process.ProcessLauncher;
+import org.sonar.application.process.Lifecycle;
+import org.sonar.application.process.ProcessEventListener;
+import org.sonar.application.process.ProcessLifecycleListener;
+import org.sonar.application.process.ProcessMonitor;
+import org.sonar.application.process.SQProcess;
+import org.sonar.process.ProcessId;
+
+public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLifecycleListener, AppStateListener {
+
+ private static final Logger LOG = LoggerFactory.getLogger(SchedulerImpl.class);
+
+ private final AppSettings settings;
+ private final AppReloader appReloader;
+ private final CommandFactory commandFactory;
+ private final ProcessLauncher processLauncher;
+ private final AppState appState;
+ private final NodeLifecycle nodeLifecycle = new NodeLifecycle();
+
+ private final CountDownLatch keepAlive = new CountDownLatch(1);
+ private final AtomicBoolean restartRequested = new AtomicBoolean(false);
+ private final AtomicBoolean restartDisabled = new AtomicBoolean(false);
+ private final EnumMap<ProcessId, SQProcess> processesById = new EnumMap<>(ProcessId.class);
+ private final AtomicInteger operationalCountDown = new AtomicInteger();
+ private final AtomicInteger stopCountDown = new AtomicInteger(0);
+ private StopperThread stopperThread;
+ private RestarterThread restarterThread;
+ private long processWatcherDelayMs = SQProcess.DEFAULT_WATCHER_DELAY_MS;
+
+ public SchedulerImpl(AppSettings settings, AppReloader appReloader, CommandFactory commandFactory,
+ ProcessLauncher processLauncher,
+ AppState appState) {
+ this.settings = settings;
+ this.appReloader = appReloader;
+ this.commandFactory = commandFactory;
+ this.processLauncher = processLauncher;
+ this.appState = appState;
+ this.appState.addListener(this);
+ }
+
+ SchedulerImpl setProcessWatcherDelayMs(long l) {
+ this.processWatcherDelayMs = l;
+ return this;
+ }
+
+ @Override
+ public void schedule() {
+ if (!nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STARTING)) {
+ return;
+ }
+ processesById.clear();
+
+ for (ProcessId processId : ClusterSettings.getEnabledProcesses(settings)) {
+ SQProcess process = SQProcess.builder(processId)
+ .addProcessLifecycleListener(this)
+ .addEventListener(this)
+ .setWatcherDelayMs(processWatcherDelayMs)
+ .build();
+ processesById.put(process.getProcessId(), process);
+ }
+ operationalCountDown.set(processesById.size());
+
+ tryToStartAll();
+ }
+
+ private void tryToStartAll() {
+ tryToStartEs();
+ tryToStartWeb();
+ tryToStartCe();
+ }
+
+ private void tryToStartEs() {
+ SQProcess process = processesById.get(ProcessId.ELASTICSEARCH);
+ if (process != null) {
+ tryToStartEsProcess(process, commandFactory::createEsCommand);
+ }
+ }
+
+ private void tryToStartWeb() {
+ SQProcess process = processesById.get(ProcessId.WEB_SERVER);
+ if (process == null || !isEsClientStartable()) {
+ return;
+ }
+ if (appState.isOperational(ProcessId.WEB_SERVER, false)) {
+ tryToStartJavaProcess(process, () -> commandFactory.createWebCommand(false));
+ } else if (appState.tryToLockWebLeader()) {
+ tryToStartJavaProcess(process, () -> commandFactory.createWebCommand(true));
+ } else {
+ Optional<String> leader = appState.getLeaderHostName();
+ if (leader.isPresent()) {
+ LOG.info("Waiting for initialization from " + leader.get());
+ } else {
+ LOG.error("Initialization failed. All nodes must be restarted");
+ }
+ }
+ }
+
+ private void tryToStartCe() {
+ SQProcess process = processesById.get(ProcessId.COMPUTE_ENGINE);
+ if (process != null && appState.isOperational(ProcessId.WEB_SERVER, false) && isEsClientStartable()) {
+ tryToStartJavaProcess(process, commandFactory::createCeCommand);
+ }
+ }
+
+ private boolean isEsClientStartable() {
+ boolean requireLocalEs = ClusterSettings.isLocalElasticsearchEnabled(settings);
+ return appState.isOperational(ProcessId.ELASTICSEARCH, requireLocalEs);
+ }
+
+ private void tryToStartJavaProcess(SQProcess process, Supplier<JavaCommand> commandSupplier) {
+ tryToStart(process, () -> {
+ JavaCommand command = commandSupplier.get();
+ return processLauncher.launch(command);
+ });
+ }
+
+ private void tryToStartEsProcess(SQProcess process, Supplier<EsCommand> commandSupplier) {
+ tryToStart(process, () -> {
+ EsCommand command = commandSupplier.get();
+ return processLauncher.launch(command);
+ });
+ }
+
+ private void tryToStart(SQProcess process, Supplier<ProcessMonitor> processMonitorSupplier) {
+ try {
+ process.start(processMonitorSupplier);
+ } catch (RuntimeException e) {
+ // failed to start command -> stop everything
+ terminate();
+ throw e;
+ }
+ }
+
+ private void stopAll() {
+ // order is important for non-cluster mode
+ stopProcess(ProcessId.COMPUTE_ENGINE);
+ stopProcess(ProcessId.WEB_SERVER);
+ stopProcess(ProcessId.ELASTICSEARCH);
+ }
+
+ /**
+ * Request for graceful stop then blocks until process is stopped.
+ * Returns immediately if the process is disabled in configuration.
+ */
+ private void stopProcess(ProcessId processId) {
+ SQProcess process = processesById.get(processId);
+ if (process != null) {
+ process.stop(1, TimeUnit.MINUTES);
+ }
+ }
+
+ /**
+ * Blocks until all processes are stopped. Pending restart, if
+ * any, is disabled.
+ */
+ @Override
+ public void terminate() {
+ // disable ability to request for restart
+ restartRequested.set(false);
+ restartDisabled.set(true);
+
+ if (nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STOPPING)) {
+ LOG.info("Stopping SonarQube");
+ }
+ stopAll();
+ if (stopperThread != null) {
+ stopperThread.interrupt();
+ }
+ if (restarterThread != null) {
+ restarterThread.interrupt();
+ }
+ keepAlive.countDown();
+ }
+
+ @Override
+ public void awaitTermination() {
+ try {
+ keepAlive.await();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ @Override
+ public void onProcessEvent(ProcessId processId, Type type) {
+ if (type == Type.OPERATIONAL) {
+ onProcessOperational(processId);
+ } else if (type == Type.ASK_FOR_RESTART && restartRequested.compareAndSet(false, true)) {
+ stopAsync();
+ }
+ }
+
+ private void onProcessOperational(ProcessId processId) {
+ LOG.info("Process[{}] is up", processId.getKey());
+ appState.setOperational(processId);
+ if (operationalCountDown.decrementAndGet() == 0 && nodeLifecycle.tryToMoveTo(NodeLifecycle.State.OPERATIONAL)) {
+ LOG.info("SonarQube is up");
+ }
+ }
+
+ @Override
+ public void onAppStateOperational(ProcessId processId) {
+ if (nodeLifecycle.getState() == NodeLifecycle.State.STARTING) {
+ tryToStartAll();
+ }
+ }
+
+ @Override
+ public void onProcessState(ProcessId processId, Lifecycle.State to) {
+ switch (to) {
+ case STOPPED:
+ onProcessStop(processId);
+ break;
+ case STARTING:
+ stopCountDown.incrementAndGet();
+ break;
+ default:
+ // Nothing to do
+ break;
+ }
+ }
+
+ private void onProcessStop(ProcessId processId) {
+ LOG.info("Process [{}] is stopped", processId.getKey());
+ if (stopCountDown.decrementAndGet() == 0 && nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STOPPED)) {
+ if (!restartDisabled.get() &&
+ restartRequested.compareAndSet(true, false)) {
+ LOG.info("SonarQube is restarting");
+ restartAsync();
+ } else {
+ LOG.info("SonarQube is stopped");
+ // all processes are stopped, no restart requested
+ // Let's clean-up resources
+ terminate();
+ }
+
+ } else if (nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STOPPING)) {
+ // this is the first process stopping
+ stopAsync();
+ }
+ }
+
+ private void stopAsync() {
+ stopperThread = new StopperThread();
+ stopperThread.start();
+ }
+
+ private void restartAsync() {
+ restarterThread = new RestarterThread();
+ restarterThread.start();
+ }
+
+ private class RestarterThread extends Thread {
+ public RestarterThread() {
+ super("Restarter");
+ }
+
+ @Override
+ public void run() {
+ try {
+ appReloader.reload(settings);
+ schedule();
+ } catch (Exception e) {
+ LOG.error("Fail to restart", e);
+ terminate();
+ }
+ }
+ }
+
+ private class StopperThread extends Thread {
+ public StopperThread() {
+ super("Stopper");
+ }
+
+ @Override
+ public void run() {
+ stopAll();
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.cluster;
+
+import java.util.EnumMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.annotation.Nonnull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.application.AppState;
+import org.sonar.application.AppStateListener;
+import org.sonar.application.config.AppSettings;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+
+public class AppStateClusterImpl implements AppState {
+ private static Logger LOGGER = LoggerFactory.getLogger(AppStateClusterImpl.class);
+
+ private final Map<ProcessId, Boolean> localProcesses = new EnumMap<>(ProcessId.class);
+ private final HazelcastCluster hazelcastCluster;
+
+ public AppStateClusterImpl(AppSettings appSettings) {
+ ClusterProperties clusterProperties = new ClusterProperties(appSettings);
+ clusterProperties.validate();
+
+ if (!clusterProperties.isEnabled()) {
+ throw new IllegalStateException("Cluster is not enabled on this instance");
+ }
+
+ hazelcastCluster = HazelcastCluster.create(clusterProperties);
+ // Add the local endpoint to be used by processes
+ appSettings.getProps().set(ProcessProperties.CLUSTER_LOCALENDPOINT, hazelcastCluster.getLocalEndPoint());
+ appSettings.getProps().set(ProcessProperties.CLUSTER_MEMBERUUID, hazelcastCluster.getLocalUUID());
+
+ String members = hazelcastCluster.getMembers().stream().collect(Collectors.joining(","));
+ LOGGER.info("Joined the cluster [{}] that contains the following hosts : [{}]", hazelcastCluster.getName(), members);
+ }
+
+ @Override
+ public void addListener(@Nonnull AppStateListener listener) {
+ hazelcastCluster.addListener(listener);
+ }
+
+ @Override
+ public boolean isOperational(@Nonnull ProcessId processId, boolean local) {
+ if (local) {
+ return localProcesses.computeIfAbsent(processId, p -> false);
+ }
+ return hazelcastCluster.isOperational(processId);
+ }
+
+ @Override
+ public void setOperational(@Nonnull ProcessId processId) {
+ localProcesses.put(processId, true);
+ hazelcastCluster.setOperational(processId);
+ }
+
+ @Override
+ public boolean tryToLockWebLeader() {
+ return hazelcastCluster.tryToLockWebLeader();
+ }
+
+ @Override
+ public void reset() {
+ throw new IllegalStateException("state reset is not supported in cluster mode");
+ }
+
+ @Override
+ public void close() {
+ hazelcastCluster.close();
+ }
+
+ @Override
+ public void registerSonarQubeVersion(String sonarqubeVersion) {
+ hazelcastCluster.registerSonarQubeVersion(sonarqubeVersion);
+ }
+
+ @Override
+ public Optional<String> getLeaderHostName() {
+ return hazelcastCluster.getLeaderHostName();
+ }
+
+ HazelcastCluster getHazelcastCluster() {
+ return hazelcastCluster;
+ }
+
+ /**
+ * Only used for testing purpose
+ *
+ * @param logger
+ */
+ static void setLogger(Logger logger) {
+ AppStateClusterImpl.LOGGER = logger;
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.cluster;
+
+import java.io.Serializable;
+import org.sonar.process.ProcessId;
+
+import static java.util.Objects.requireNonNull;
+
+public class ClusterProcess implements Serializable {
+ private final ProcessId processId;
+ private final String nodeUuid;
+
+ public ClusterProcess(String nodeUuid, ProcessId processId) {
+ this.processId = requireNonNull(processId);
+ this.nodeUuid = requireNonNull(nodeUuid);
+ }
+
+ public ProcessId getProcessId() {
+ return processId;
+ }
+
+ public String getNodeUuid() {
+ return nodeUuid;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ ClusterProcess that = (ClusterProcess) o;
+ if (processId != that.processId) {
+ return false;
+ }
+ return nodeUuid.equals(that.nodeUuid);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = processId.hashCode();
+ result = 31 * result + nodeUuid.hashCode();
+ return result;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.cluster;
+
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.List;
+import javax.annotation.Nullable;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.application.config.AppSettings;
+import org.sonar.process.ProcessProperties;
+
+/**
+ * Properties of the cluster configuration
+ */
+public final class ClusterProperties {
+ static final String DEFAULT_PORT = "9003";
+ private static final Logger LOGGER = LoggerFactory.getLogger(ClusterProperties.class);
+
+ private final int port;
+ private final boolean enabled;
+ private final List<String> hosts;
+ private final List<String> networkInterfaces;
+ private final String name;
+
+ ClusterProperties(AppSettings appSettings) {
+ port = appSettings.getProps().valueAsInt(ProcessProperties.CLUSTER_PORT);
+ enabled = appSettings.getProps().valueAsBoolean(ProcessProperties.CLUSTER_ENABLED);
+ networkInterfaces = extractNetworkInterfaces(
+ appSettings.getProps().value(ProcessProperties.CLUSTER_NETWORK_INTERFACES, "")
+ );
+ name = appSettings.getProps().nonNullValue(ProcessProperties.CLUSTER_NAME);
+ hosts = extractHosts(
+ appSettings.getProps().value(ProcessProperties.CLUSTER_HOSTS, "")
+ );
+ }
+
+ int getPort() {
+ return port;
+ }
+
+ boolean isEnabled() {
+ return enabled;
+ }
+
+ List<String> getHosts() {
+ return hosts;
+ }
+
+ List<String> getNetworkInterfaces() {
+ return networkInterfaces;
+ }
+
+ String getName() {
+ return name;
+ }
+
+ void validate() {
+ if (!enabled) {
+ return;
+ }
+
+ // Test validity of port
+ checkArgument(
+ port > 0 && port < 65_536,
+ "Cluster port have been set to %d which is outside the range [1-65535].",
+ port
+ );
+
+ // Test the networkInterfaces parameter
+ try {
+ List<String> localInterfaces = findAllLocalIPs();
+
+ networkInterfaces.forEach(
+ inet -> checkArgument(
+ StringUtils.isEmpty(inet) || localInterfaces.contains(inet),
+ "Interface %s is not available on this machine.",
+ inet
+ )
+ );
+ } catch (SocketException e) {
+ LOGGER.warn("Unable to retrieve network networkInterfaces. Interfaces won't be checked", e);
+ }
+ }
+
+ private static List<String> extractHosts(final String hosts) {
+ List<String> result = new ArrayList<>();
+ for (String host : hosts.split(",")) {
+ if (StringUtils.isNotEmpty(host)) {
+ if (!host.contains(":")) {
+ result.add(
+ String.format("%s:%s", host, DEFAULT_PORT)
+ );
+ } else {
+ result.add(host);
+ }
+ }
+ }
+ return result;
+ }
+
+ private static List<String> extractNetworkInterfaces(final String networkInterfaces) {
+ List<String> result = new ArrayList<>();
+ for (String iface : networkInterfaces.split(",")) {
+ if (StringUtils.isNotEmpty(iface)) {
+ result.add(iface);
+ }
+ }
+ return result;
+ }
+
+ private static List<String> findAllLocalIPs() throws SocketException {
+ Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
+ List<String> localInterfaces = new ArrayList<>();
+
+ while (netInterfaces.hasMoreElements()) {
+ NetworkInterface networkInterface = netInterfaces.nextElement();
+ Enumeration<InetAddress> ips = networkInterface.getInetAddresses();
+ while (ips.hasMoreElements()) {
+ InetAddress ip = ips.nextElement();
+ localInterfaces.add(ip.getHostAddress());
+ }
+ }
+ return localInterfaces;
+ }
+
+ private static void checkArgument(boolean expression,
+ @Nullable String messageTemplate,
+ @Nullable Object... args) {
+ if (!expression) {
+ throw new IllegalArgumentException(String.format(messageTemplate, args));
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.cluster;
+
+import com.hazelcast.config.Config;
+import com.hazelcast.config.JoinConfig;
+import com.hazelcast.config.NetworkConfig;
+import com.hazelcast.core.Client;
+import com.hazelcast.core.ClientListener;
+import com.hazelcast.core.EntryEvent;
+import com.hazelcast.core.EntryListener;
+import com.hazelcast.core.Hazelcast;
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.core.HazelcastInstanceNotActiveException;
+import com.hazelcast.core.IAtomicReference;
+import com.hazelcast.core.ILock;
+import com.hazelcast.core.MapEvent;
+import com.hazelcast.core.Member;
+import com.hazelcast.core.ReplicatedMap;
+import com.hazelcast.nio.Address;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.application.AppStateListener;
+import org.sonar.process.ProcessId;
+
+import static java.util.stream.Collectors.toList;
+import static org.sonar.process.NetworkUtils.getHostName;
+import static org.sonar.process.cluster.ClusterObjectKeys.CLIENT_UUIDS;
+import static org.sonar.process.cluster.ClusterObjectKeys.HOSTNAME;
+import static org.sonar.process.cluster.ClusterObjectKeys.LEADER;
+import static org.sonar.process.cluster.ClusterObjectKeys.OPERATIONAL_PROCESSES;
+import static org.sonar.process.cluster.ClusterObjectKeys.SONARQUBE_VERSION;
+
+public class HazelcastCluster implements AutoCloseable {
+ private static final Logger LOGGER = LoggerFactory.getLogger(HazelcastCluster.class);
+
+ private final List<AppStateListener> listeners = new ArrayList<>();
+ private final ReplicatedMap<ClusterProcess, Boolean> operationalProcesses;
+ private final String operationalProcessListenerUUID;
+ private final String clientListenerUUID;
+
+ protected final HazelcastInstance hzInstance;
+
+ private HazelcastCluster(Config hzConfig) {
+ // Create the Hazelcast instance
+ hzInstance = Hazelcast.newHazelcastInstance(hzConfig);
+
+ // Get or create the replicated map
+ operationalProcesses = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
+ operationalProcessListenerUUID = operationalProcesses.addEntryListener(new OperationalProcessListener());
+ clientListenerUUID = hzInstance.getClientService().addClientListener(new ConnectedClientListener());
+ }
+
+ String getLocalUUID() {
+ return hzInstance.getLocalEndpoint().getUuid();
+ }
+
+ String getName() {
+ return hzInstance.getConfig().getGroupConfig().getName();
+ }
+
+ List<String> getMembers() {
+ return hzInstance.getCluster().getMembers().stream()
+ .filter(m -> !m.localMember())
+ .map(m -> m.getStringAttribute(HOSTNAME))
+ .collect(toList());
+ }
+
+ void addListener(AppStateListener listener) {
+ listeners.add(listener);
+ }
+
+ boolean isOperational(ProcessId processId) {
+ for (Map.Entry<ClusterProcess, Boolean> entry : operationalProcesses.entrySet()) {
+ if (entry.getKey().getProcessId().equals(processId) && entry.getValue()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ void setOperational(ProcessId processId) {
+ operationalProcesses.put(new ClusterProcess(getLocalUUID(), processId), Boolean.TRUE);
+ }
+
+ boolean tryToLockWebLeader() {
+ IAtomicReference<String> leader = hzInstance.getAtomicReference(LEADER);
+ if (leader.get() == null) {
+ ILock lock = hzInstance.getLock(LEADER);
+ lock.lock();
+ try {
+ if (leader.get() == null) {
+ leader.set(getLocalUUID());
+ return true;
+ } else {
+ return false;
+ }
+ } finally {
+ lock.unlock();
+ }
+ } else {
+ return false;
+ }
+ }
+
+ public void registerSonarQubeVersion(String sonarqubeVersion) {
+ IAtomicReference<String> sqVersion = hzInstance.getAtomicReference(SONARQUBE_VERSION);
+ if (sqVersion.get() == null) {
+ ILock lock = hzInstance.getLock(SONARQUBE_VERSION);
+ lock.lock();
+ try {
+ if (sqVersion.get() == null) {
+ sqVersion.set(sonarqubeVersion);
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ String clusterVersion = sqVersion.get();
+ if (!sqVersion.get().equals(sonarqubeVersion)) {
+ throw new IllegalStateException(
+ String.format("The local version %s is not the same as the cluster %s", sonarqubeVersion, clusterVersion)
+ );
+ }
+ }
+
+ @Override
+ public void close() {
+ if (hzInstance != null) {
+ try {
+ // Removing listeners
+ operationalProcesses.removeEntryListener(operationalProcessListenerUUID);
+ hzInstance.getClientService().removeClientListener(clientListenerUUID);
+
+ // Removing the operationalProcess from the replicated map
+ operationalProcesses.keySet().forEach(
+ clusterNodeProcess -> {
+ if (clusterNodeProcess.getNodeUuid().equals(getLocalUUID())) {
+ operationalProcesses.remove(clusterNodeProcess);
+ }
+ });
+
+ // Shutdown Hazelcast properly
+ hzInstance.shutdown();
+ } catch (HazelcastInstanceNotActiveException e) {
+ // hazelcastCluster may be already closed by the shutdown hook
+ LOGGER.debug("Unable to close Hazelcast cluster", e);
+ }
+ }
+ }
+
+ public static HazelcastCluster create(ClusterProperties clusterProperties) {
+ Config hzConfig = new Config();
+ hzConfig.getGroupConfig().setName(clusterProperties.getName());
+
+ // Configure the network instance
+ NetworkConfig netConfig = hzConfig.getNetworkConfig();
+ netConfig
+ .setPort(clusterProperties.getPort())
+ .setReuseAddress(true);
+
+ if (!clusterProperties.getNetworkInterfaces().isEmpty()) {
+ netConfig.getInterfaces()
+ .setEnabled(true)
+ .setInterfaces(clusterProperties.getNetworkInterfaces());
+ }
+
+ // Only allowing TCP/IP configuration
+ JoinConfig joinConfig = netConfig.getJoin();
+ joinConfig.getAwsConfig().setEnabled(false);
+ joinConfig.getMulticastConfig().setEnabled(false);
+ joinConfig.getTcpIpConfig().setEnabled(true);
+ joinConfig.getTcpIpConfig().setMembers(clusterProperties.getHosts());
+
+ // Tweak HazelCast configuration
+ hzConfig
+ // Increase the number of tries
+ .setProperty("hazelcast.tcp.join.port.try.count", "10")
+ // Don't bind on all interfaces
+ .setProperty("hazelcast.socket.bind.any", "false")
+ // Don't phone home
+ .setProperty("hazelcast.phone.home.enabled", "false")
+ // Use slf4j for logging
+ .setProperty("hazelcast.logging.type", "slf4j");
+
+ // Trying to resolve the hostname
+ hzConfig.getMemberAttributeConfig().setStringAttribute(HOSTNAME, getHostName());
+
+ // We are not using the partition group of Hazelcast, so disabling it
+ hzConfig.getPartitionGroupConfig().setEnabled(false);
+ return new HazelcastCluster(hzConfig);
+ }
+
+ Optional<String> getLeaderHostName() {
+ String leaderId = (String) hzInstance.getAtomicReference(LEADER).get();
+ if (leaderId != null) {
+ Optional<Member> leader = hzInstance.getCluster().getMembers().stream().filter(m -> m.getUuid().equals(leaderId)).findFirst();
+ if (leader.isPresent()) {
+ return Optional.of(leader.get().getStringAttribute(HOSTNAME));
+ }
+ }
+ return Optional.empty();
+ }
+
+ String getLocalEndPoint() {
+ Address localAddress = hzInstance.getCluster().getLocalMember().getAddress();
+ return String.format("%s:%d", localAddress.getHost(), localAddress.getPort());
+ }
+
+ private class OperationalProcessListener implements EntryListener<ClusterProcess, Boolean> {
+ @Override
+ public void entryAdded(EntryEvent<ClusterProcess, Boolean> event) {
+ if (event.getValue()) {
+ listeners.forEach(appStateListener -> appStateListener.onAppStateOperational(event.getKey().getProcessId()));
+ }
+ }
+
+ @Override
+ public void entryRemoved(EntryEvent<ClusterProcess, Boolean> event) {
+ // Ignore it
+ }
+
+ @Override
+ public void entryUpdated(EntryEvent<ClusterProcess, Boolean> event) {
+ if (event.getValue()) {
+ listeners.forEach(appStateListener -> appStateListener.onAppStateOperational(event.getKey().getProcessId()));
+ }
+ }
+
+ @Override
+ public void entryEvicted(EntryEvent<ClusterProcess, Boolean> event) {
+ // Ignore it
+ }
+
+ @Override
+ public void mapCleared(MapEvent event) {
+ // Ignore it
+ }
+
+ @Override
+ public void mapEvicted(MapEvent event) {
+ // Ignore it
+ }
+ }
+
+ private class ConnectedClientListener implements ClientListener {
+ @Override
+ public void clientConnected(Client client) {
+ hzInstance.getSet(CLIENT_UUIDS).add(client.getUuid());
+ }
+
+ @Override
+ public void clientDisconnected(Client client) {
+ hzInstance.getSet(CLIENT_UUIDS).remove(client.getUuid());
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.application.cluster;
+
+import javax.annotation.ParametersAreNonnullByDefault;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.util.Optional;
+import org.sonar.process.Props;
+
+public interface AppSettings {
+
+ Props getProps();
+
+ Optional<String> getValue(String key);
+
+ void reload(Props copy);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.util.Optional;
+import org.sonar.process.Props;
+
+public class AppSettingsImpl implements AppSettings {
+
+ private Props props;
+
+ AppSettingsImpl(Props props) {
+ this.props = props;
+ }
+
+ @Override
+ public Props getProps() {
+ return props;
+ }
+
+ @Override
+ public Optional<String> getValue(String key) {
+ return Optional.ofNullable(props.value(key));
+ }
+
+ @Override
+ public void reload(Props copy) {
+ this.props = copy;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+public interface AppSettingsLoader {
+
+ AppSettings load();
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.URISyntaxException;
+import java.util.Arrays;
+import java.util.Properties;
+import java.util.function.Consumer;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.ConfigurationUtils;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+public class AppSettingsLoaderImpl implements AppSettingsLoader {
+
+ private final File homeDir;
+ private final String[] cliArguments;
+ private final Consumer<Props>[] consumers;
+
+ public AppSettingsLoaderImpl(String[] cliArguments) {
+ this(cliArguments, detectHomeDir(), new FileSystemSettings(), new JdbcSettings(), new ClusterSettings());
+ }
+
+ AppSettingsLoaderImpl(String[] cliArguments, File homeDir, Consumer<Props>... consumers) {
+ this.cliArguments = cliArguments;
+ this.homeDir = homeDir;
+ this.consumers = consumers;
+ }
+
+ File getHomeDir() {
+ return homeDir;
+ }
+
+ @Override
+ public AppSettings load() {
+ Properties p = loadPropertiesFile(homeDir);
+ p.putAll(CommandLineParser.parseArguments(cliArguments));
+ p.setProperty(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+ p = ConfigurationUtils.interpolateVariables(p, System.getenv());
+
+ // the difference between Properties and Props is that the latter
+ // supports decryption of values, so it must be used when values
+ // are accessed
+ Props props = new Props(p);
+ ProcessProperties.completeDefaults(props);
+ Arrays.stream(consumers).forEach(c -> c.accept(props));
+
+ return new AppSettingsImpl(props);
+ }
+
+ private static File detectHomeDir() {
+ try {
+ File appJar = new File(AppSettingsLoaderImpl.class.getProtectionDomain().getCodeSource().getLocation().toURI());
+ return appJar.getParentFile().getParentFile();
+ } catch (URISyntaxException e) {
+ throw new IllegalStateException("Cannot detect path of main jar file", e);
+ }
+ }
+
+ /**
+ * Loads the configuration file ${homeDir}/conf/sonar.properties.
+ * An empty {@link Properties} is returned if the file does not exist.
+ */
+ private static Properties loadPropertiesFile(File homeDir) {
+ Properties p = new Properties();
+ File propsFile = new File(homeDir, "conf/sonar.properties");
+ if (propsFile.exists()) {
+ try (Reader reader = new InputStreamReader(new FileInputStream(propsFile), UTF_8)) {
+ p.load(reader);
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot open file " + propsFile, e);
+ }
+ } else {
+ LoggerFactory.getLogger(AppSettingsLoaderImpl.class).warn("Configuration file not found: {}", propsFile);
+ }
+ return p;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.net.HostAndPort;
+import com.google.common.net.InetAddresses;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Consumer;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.process.MessageException;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+import static com.google.common.net.InetAddresses.forString;
+import static java.lang.String.format;
+import static java.util.Arrays.stream;
+import static org.apache.commons.lang.StringUtils.isBlank;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NETWORK_INTERFACES;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_WEB_LEADER;
+import static org.sonar.process.ProcessProperties.JDBC_URL;
+import static org.sonar.process.ProcessProperties.SEARCH_HOST;
+
+public class ClusterSettings implements Consumer<Props> {
+
+ @Override
+ public void accept(Props props) {
+ if (!isClusterEnabled(props)) {
+ return;
+ }
+
+ checkProperties(props);
+ }
+
+ private static void checkProperties(Props props) {
+ // Cluster web leader is not allowed
+ if (props.value(CLUSTER_WEB_LEADER) != null) {
+ throw new MessageException(format("Property [%s] is forbidden", CLUSTER_WEB_LEADER));
+ }
+
+ if (props.valueAsBoolean(ProcessProperties.CLUSTER_ENABLED) &&
+ !props.valueAsBoolean(ProcessProperties.CLUSTER_SEARCH_DISABLED, false)
+ ) {
+ ensureMandatoryProperty(props, SEARCH_HOST);
+ ensureNotLoopback(props, SEARCH_HOST);
+ }
+ // Mandatory properties
+ ensureMandatoryProperty(props, CLUSTER_HOSTS);
+ ensureMandatoryProperty(props, CLUSTER_SEARCH_HOSTS);
+
+ // H2 Database is not allowed in cluster mode
+ String jdbcUrl = props.value(JDBC_URL);
+ if (isBlank(jdbcUrl) || jdbcUrl.startsWith("jdbc:h2:")) {
+ throw new MessageException("Embedded database is not supported in cluster mode");
+ }
+
+ // Loopback interfaces are forbidden for SEARCH_HOST and CLUSTER_NETWORK_INTERFACES
+ ensureNotLoopback(props, CLUSTER_HOSTS);
+ ensureNotLoopback(props, CLUSTER_NETWORK_INTERFACES);
+ ensureNotLoopback(props, CLUSTER_SEARCH_HOSTS);
+
+ ensureLocalAddress(props, SEARCH_HOST);
+ ensureLocalAddress(props, CLUSTER_NETWORK_INTERFACES);
+ }
+
+ private static void ensureMandatoryProperty(Props props, String key) {
+ if (isBlank(props.value(key))) {
+ throw new MessageException(format("Property [%s] is mandatory", key));
+ }
+ }
+
+ @VisibleForTesting
+ protected static void ensureNotLoopback(Props props, String key) {
+ String ipList = props.value(key);
+ if (ipList == null) {
+ return;
+ }
+
+ stream(ipList.split(","))
+ .filter(StringUtils::isNotBlank)
+ .map(StringUtils::trim)
+ .forEach(ip -> {
+ InetAddress inetAddress = convertToInetAddress(ip, key);
+ if (inetAddress.isLoopbackAddress()) {
+ throw new MessageException(format("The interface address [%s] of [%s] must not be a loopback address", ip, key));
+ }
+ });
+ }
+
+ private static void ensureLocalAddress(Props props, String key) {
+ String ipList = props.value(key);
+
+ if (ipList == null) {
+ return;
+ }
+
+ stream(ipList.split(","))
+ .filter(StringUtils::isNotBlank)
+ .map(StringUtils::trim)
+ .forEach(ip -> {
+ InetAddress inetAddress = convertToInetAddress(ip, key);
+ try {
+ if (NetworkInterface.getByInetAddress(inetAddress) == null) {
+ throw new MessageException(format("The interface address [%s] of [%s] is not a local address", ip, key));
+ }
+ } catch (SocketException e) {
+ throw new MessageException(format("The interface address [%s] of [%s] is not a local address", ip, key));
+ }
+ });
+ }
+
+ private static InetAddress convertToInetAddress(String text, String key) {
+ InetAddress inetAddress;
+ HostAndPort hostAndPort = HostAndPort.fromString(text);
+ if (!InetAddresses.isInetAddress(hostAndPort.getHostText())) {
+ try {
+ inetAddress =InetAddress.getByName(hostAndPort.getHostText());
+ } catch (UnknownHostException e) {
+ throw new MessageException(format("The interface address [%s] of [%s] cannot be resolved : %s", text, key, e.getMessage()));
+ }
+ } else {
+ inetAddress = forString(hostAndPort.getHostText());
+ }
+
+ return inetAddress;
+ }
+
+ public static boolean isClusterEnabled(AppSettings settings) {
+ return isClusterEnabled(settings.getProps());
+ }
+
+ private static boolean isClusterEnabled(Props props) {
+ return props.valueAsBoolean(CLUSTER_ENABLED);
+ }
+
+ public static List<ProcessId> getEnabledProcesses(AppSettings settings) {
+ if (!isClusterEnabled(settings)) {
+ return Arrays.asList(ProcessId.ELASTICSEARCH, ProcessId.WEB_SERVER, ProcessId.COMPUTE_ENGINE);
+ }
+ List<ProcessId> enabled = new ArrayList<>();
+ if (!settings.getProps().valueAsBoolean(ProcessProperties.CLUSTER_SEARCH_DISABLED)) {
+ enabled.add(ProcessId.ELASTICSEARCH);
+ }
+ if (!settings.getProps().valueAsBoolean(ProcessProperties.CLUSTER_WEB_DISABLED)) {
+ enabled.add(ProcessId.WEB_SERVER);
+ }
+
+ if (!settings.getProps().valueAsBoolean(ProcessProperties.CLUSTER_CE_DISABLED)) {
+ enabled.add(ProcessId.COMPUTE_ENGINE);
+ }
+ return enabled;
+ }
+
+ public static boolean isLocalElasticsearchEnabled(AppSettings settings) {
+ return !isClusterEnabled(settings.getProps()) ||
+ !settings.getProps().valueAsBoolean(ProcessProperties.CLUSTER_SEARCH_DISABLED);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.Map;
+import java.util.Properties;
+
+public class CommandLineParser {
+
+ private CommandLineParser() {
+ // prevent instantiation
+ }
+
+ /**
+ * Build properties from command-line arguments and system properties
+ */
+ public static Properties parseArguments(String[] args) {
+ Properties props = argumentsToProperties(args);
+
+ // complete with only the system properties that start with "sonar."
+ for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
+ String key = entry.getKey().toString();
+ if (key.startsWith("sonar.")) {
+ props.setProperty(key, entry.getValue().toString());
+ }
+ }
+ return props;
+ }
+
+ /**
+ * Convert strings "-Dkey=value" to properties
+ */
+ static Properties argumentsToProperties(String[] args) {
+ Properties props = new Properties();
+ for (String arg : args) {
+ if (!arg.startsWith("-D") || !arg.contains("=")) {
+ throw new IllegalArgumentException(String.format(
+ "Command-line argument must start with -D, for example -Dsonar.jdbc.username=sonar. Got: %s", arg));
+ }
+ String key = StringUtils.substringBefore(arg, "=").substring(2);
+ String value = StringUtils.substringAfter(arg, "=");
+ props.setProperty(key, value);
+ }
+ return props;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.io.File;
+import java.util.function.Consumer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.Props;
+
+import static org.sonar.process.ProcessProperties.PATH_DATA;
+import static org.sonar.process.ProcessProperties.PATH_HOME;
+import static org.sonar.process.ProcessProperties.PATH_LOGS;
+import static org.sonar.process.ProcessProperties.PATH_TEMP;
+import static org.sonar.process.ProcessProperties.PATH_WEB;
+
+public class FileSystemSettings implements Consumer<Props> {
+
+ private static final Logger LOG = LoggerFactory.getLogger(FileSystemSettings.class);
+
+ @Override
+ public void accept(Props props) {
+ ensurePropertyIsAbsolutePath(props, PATH_DATA);
+ ensurePropertyIsAbsolutePath(props, PATH_WEB);
+ ensurePropertyIsAbsolutePath(props, PATH_LOGS);
+ ensurePropertyIsAbsolutePath(props, PATH_TEMP);
+ }
+
+ private static File ensurePropertyIsAbsolutePath(Props props, String propKey) {
+ File homeDir = props.nonNullValueAsFile(PATH_HOME);
+ // default values are set by ProcessProperties
+ String path = props.nonNullValue(propKey);
+ File d = new File(path);
+ if (!d.isAbsolute()) {
+ d = new File(homeDir, path);
+ LOG.trace("Overriding property {} from relative path '{}' to absolute path '{}'", path, d.getAbsolutePath());
+ props.set(propKey, d.getAbsolutePath());
+ }
+ return d;
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.io.File;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.MessageException;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.isEmpty;
+import static org.apache.commons.lang.StringUtils.isNotEmpty;
+import static org.sonar.process.ProcessProperties.JDBC_EMBEDDED_PORT;
+import static org.sonar.process.ProcessProperties.JDBC_URL;
+
+public class JdbcSettings implements Consumer<Props> {
+
+ private static final int JDBC_EMBEDDED_PORT_DEFAULT_VALUE = 9092;
+
+ enum Provider {
+ H2("lib/jdbc/h2"), SQLSERVER("lib/jdbc/mssql"), MYSQL("lib/jdbc/mysql"), ORACLE("extensions/jdbc-driver/oracle"),
+ POSTGRESQL("lib/jdbc/postgresql");
+
+ final String path;
+
+ Provider(String path) {
+ this.path = path;
+ }
+ }
+
+ @Override
+ public void accept(Props props) {
+ File homeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME);
+ Provider provider = resolveProviderAndEnforceNonnullJdbcUrl(props);
+ String url = props.value(JDBC_URL);
+ checkUrlParameters(provider, url);
+ String driverPath = driverPath(homeDir, provider);
+ props.set(ProcessProperties.JDBC_DRIVER_PATH, driverPath);
+ }
+
+ String driverPath(File homeDir, Provider provider) {
+ String dirPath = provider.path;
+ File dir = new File(homeDir, dirPath);
+ if (!dir.exists()) {
+ throw new MessageException("Directory does not exist: " + dirPath);
+ }
+ List<File> files = new ArrayList<>(FileUtils.listFiles(dir, new String[] {"jar"}, false));
+ if (files.isEmpty()) {
+ throw new MessageException("Directory does not contain JDBC driver: " + dirPath);
+ }
+ if (files.size() > 1) {
+ throw new MessageException("Directory must contain only one JAR file: " + dirPath);
+ }
+ return files.get(0).getAbsolutePath();
+ }
+
+ Provider resolveProviderAndEnforceNonnullJdbcUrl(Props props) {
+ String url = props.value(JDBC_URL);
+ Integer embeddedDatabasePort = props.valueAsInt(JDBC_EMBEDDED_PORT);
+
+ if (embeddedDatabasePort != null) {
+ String correctUrl = buildH2JdbcUrl(embeddedDatabasePort);
+ warnIfUrlIsSet(embeddedDatabasePort, url, correctUrl);
+ props.set(JDBC_URL, correctUrl);
+ return Provider.H2;
+ }
+
+ if (isEmpty(url)) {
+ props.set(JDBC_URL, buildH2JdbcUrl(JDBC_EMBEDDED_PORT_DEFAULT_VALUE));
+ props.set(JDBC_EMBEDDED_PORT, String.valueOf(JDBC_EMBEDDED_PORT_DEFAULT_VALUE));
+ return Provider.H2;
+ }
+
+ Pattern pattern = Pattern.compile("jdbc:(\\w+):.+");
+ Matcher matcher = pattern.matcher(url);
+ if (!matcher.find()) {
+ throw new MessageException(format("Bad format of JDBC URL: %s", url));
+ }
+ String key = matcher.group(1);
+ try {
+ return Provider.valueOf(StringUtils.upperCase(key));
+ } catch (IllegalArgumentException e) {
+ throw new MessageException(format("Unsupported JDBC driver provider: %s", key));
+ }
+ }
+
+ private static String buildH2JdbcUrl(int embeddedDatabasePort) {
+ InetAddress ip = InetAddress.getLoopbackAddress();
+ String host;
+ if (ip instanceof Inet6Address) {
+ host = "[" + ip.getHostAddress() + "]";
+ } else {
+ host = ip.getHostAddress();
+ }
+ return format("jdbc:h2:tcp://%s:%d/sonar", host, embeddedDatabasePort);
+ }
+
+ void checkUrlParameters(Provider provider, String url) {
+ if (Provider.MYSQL.equals(provider)) {
+ checkRequiredParameter(url, "useUnicode=true");
+ checkRequiredParameter(url, "characterEncoding=utf8");
+ checkRecommendedParameter(url, "rewriteBatchedStatements=true");
+ checkRecommendedParameter(url, "useConfigs=maxPerformance");
+ }
+ }
+
+ private static void warnIfUrlIsSet(int port, String existing, String expectedUrl) {
+ if (isNotEmpty(existing)) {
+ Logger logger = LoggerFactory.getLogger(JdbcSettings.class);
+ if (expectedUrl.equals(existing)) {
+ logger.warn("To change H2 database port, only property '{}' should be set (which current value is '{}'). " +
+ "Remove property '{}' from configuration to remove this warning.",
+ JDBC_EMBEDDED_PORT, port,
+ JDBC_URL);
+ } else {
+ logger.warn("Both '{}' and '{}' properties are set. " +
+ "The value of property '{}' ('{}') is not consistent with the value of property '{}' ('{}'). " +
+ "The value of property '{}' will be ignored and value '{}' will be used instead. " +
+ "To remove this warning, either remove property '{}' if your intent was to use the embedded H2 database, otherwise remove property '{}'.",
+ JDBC_EMBEDDED_PORT, JDBC_URL,
+ JDBC_URL, existing, JDBC_EMBEDDED_PORT, port,
+ JDBC_URL, expectedUrl,
+ JDBC_URL, JDBC_EMBEDDED_PORT);
+ }
+ }
+ }
+
+ private static void checkRequiredParameter(String url, String val) {
+ if (!url.contains(val)) {
+ throw new MessageException(format("JDBC URL must have the property '%s'", val));
+ }
+ }
+
+ private void checkRecommendedParameter(String url, String val) {
+ if (!url.contains(val)) {
+ LoggerFactory.getLogger(getClass()).warn("JDBC URL is recommended to have the property '{}'", val);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.config;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+import static java.lang.String.format;
+
+public class SonarQubeVersionHelper {
+ private static final String SONARQUBE_VERSION_PATH = "/sonarqube-version.txt";
+
+ private static String sonarqubeVersion;
+
+ private SonarQubeVersionHelper() {
+ // only static methods
+ }
+
+ public static String getSonarqubeVersion() {
+ if (sonarqubeVersion == null) {
+ loadVersion();
+ }
+ return sonarqubeVersion;
+ }
+
+ private static synchronized void loadVersion() {
+ try {
+ try (BufferedReader in = new BufferedReader(
+ new InputStreamReader(
+ SonarQubeVersionHelper.class.getResourceAsStream(SONARQUBE_VERSION_PATH),
+ StandardCharsets.UTF_8
+ ))) {
+ sonarqubeVersion = in.readLine();
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException(format("Cannot load %s from classpath", SONARQUBE_VERSION_PATH), e);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.application.config;
+
+import javax.annotation.ParametersAreNonnullByDefault;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.application;
+
+import javax.annotation.ParametersAreNonnullByDefault;
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+import javax.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.ProcessId;
+
+abstract class AbstractProcessMonitor implements ProcessMonitor {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AbstractProcessMonitor.class);
+ private static final int EXPECTED_EXIT_VALUE = 0;
+
+ protected final Process process;
+ private final ProcessId processId;
+
+ protected AbstractProcessMonitor(Process process, ProcessId processId) {
+ this.process = process;
+ this.processId = processId;
+ }
+
+ public InputStream getInputStream() {
+ return process.getInputStream();
+ }
+
+ public InputStream getErrorStream() {
+ return process.getErrorStream();
+ }
+
+ public void closeStreams() {
+ closeQuietly(process.getInputStream());
+ closeQuietly(process.getOutputStream());
+ closeQuietly(process.getErrorStream());
+ }
+
+ private static void closeQuietly(@Nullable Closeable closeable) {
+ try {
+ if (closeable != null) {
+ closeable.close();
+ }
+ } catch (IOException ignored) {
+ // ignore
+ }
+ }
+
+ public boolean isAlive() {
+ return process.isAlive();
+ }
+
+ public void destroyForcibly() {
+ process.destroyForcibly();
+ }
+
+ public void waitFor() throws InterruptedException {
+ int exitValue = process.waitFor();
+ if (exitValue != EXPECTED_EXIT_VALUE) {
+ LOG.warn("Process exited with exit value [{}]: {}", processId.getKey(), exitValue);
+ } else {
+ LOG.debug("Process exited with exit value [{}]: {}", processId.getKey(), exitValue);
+ }
+ }
+
+ public void waitFor(long timeout, TimeUnit unit) throws InterruptedException {
+ process.waitFor(timeout, unit);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import com.google.common.net.HostAndPort;
+import io.netty.util.ThreadDeathWatcher;
+import io.netty.util.concurrent.GlobalEventExecutor;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
+import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
+import org.elasticsearch.client.transport.NoNodeAvailableException;
+import org.elasticsearch.client.transport.TransportClient;
+import org.elasticsearch.cluster.health.ClusterHealthStatus;
+import org.elasticsearch.common.network.NetworkModule;
+import org.elasticsearch.common.settings.Settings;
+import org.elasticsearch.common.transport.InetSocketTransportAddress;
+import org.elasticsearch.common.transport.TransportAddress;
+import org.elasticsearch.transport.Netty4Plugin;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.ProcessId;
+import org.sonar.process.command.EsCommand;
+
+import static java.util.Collections.singletonList;
+import static java.util.Collections.unmodifiableList;
+import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds;
+import static org.sonar.application.process.EsProcessMonitor.Status.CONNECTION_REFUSED;
+import static org.sonar.application.process.EsProcessMonitor.Status.GREEN;
+import static org.sonar.application.process.EsProcessMonitor.Status.KO;
+import static org.sonar.application.process.EsProcessMonitor.Status.RED;
+import static org.sonar.application.process.EsProcessMonitor.Status.YELLOW;
+
+public class EsProcessMonitor extends AbstractProcessMonitor {
+ private static final Logger LOG = LoggerFactory.getLogger(EsProcessMonitor.class);
+ private static final int WAIT_FOR_UP_DELAY_IN_MILLIS = 100;
+ private static final int WAIT_FOR_UP_TIMEOUT = 10 * 60; /* 1min */
+
+ private final AtomicBoolean nodeUp = new AtomicBoolean(false);
+ private final AtomicBoolean nodeOperational = new AtomicBoolean(false);
+ private final EsCommand esCommand;
+ private AtomicReference<TransportClient> transportClient = new AtomicReference<>(null);
+
+ public EsProcessMonitor(Process process, ProcessId processId, EsCommand esCommand) {
+ super(process, processId);
+ this.esCommand = esCommand;
+ }
+
+ @Override
+ public boolean isOperational() {
+ if (nodeOperational.get()) {
+ return true;
+ }
+
+ boolean flag = false;
+ try {
+ flag = checkOperational();
+ } catch (InterruptedException e) {
+ LOG.trace("Interrupted while checking ES node is operational", e);
+ Thread.currentThread().interrupt();
+ } finally {
+ if (flag) {
+ transportClient.set(null);
+ nodeOperational.set(true);
+ }
+ }
+ return nodeOperational.get();
+ }
+
+ private boolean checkOperational() throws InterruptedException {
+ int i = 0;
+ Status status = checkStatus();
+ do {
+ if (status != Status.CONNECTION_REFUSED) {
+ nodeUp.set(true);
+ } else {
+ Thread.sleep(WAIT_FOR_UP_DELAY_IN_MILLIS);
+ i++;
+ status = checkStatus();
+ }
+ } while (!nodeUp.get() && i < WAIT_FOR_UP_TIMEOUT);
+ return status == YELLOW || status == GREEN;
+ }
+
+ static class MinimalTransportClient extends TransportClient {
+
+ MinimalTransportClient(Settings settings) {
+ super(settings, unmodifiableList(singletonList(Netty4Plugin.class)));
+ }
+
+ @Override
+ public void close() {
+ super.close();
+ if (NetworkModule.TRANSPORT_TYPE_SETTING.exists(settings) == false
+ || NetworkModule.TRANSPORT_TYPE_SETTING.get(settings).equals(Netty4Plugin.NETTY_TRANSPORT_NAME)) {
+ try {
+ GlobalEventExecutor.INSTANCE.awaitInactivity(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ try {
+ ThreadDeathWatcher.awaitInactivity(5, TimeUnit.SECONDS);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ }
+
+ private Status checkStatus() {
+ try {
+ ClusterHealthResponse response = getTransportClient().admin().cluster()
+ .health(new ClusterHealthRequest().waitForStatus(ClusterHealthStatus.YELLOW).timeout(timeValueSeconds(30)))
+ .actionGet();
+ if (response.getStatus() == ClusterHealthStatus.GREEN) {
+ return GREEN;
+ }
+ if (response.getStatus() == ClusterHealthStatus.YELLOW) {
+ return YELLOW;
+ }
+ if (response.getStatus() == ClusterHealthStatus.RED) {
+ return RED;
+ }
+ return KO;
+ } catch (NoNodeAvailableException e) {
+ return CONNECTION_REFUSED;
+ } catch (Exception e) {
+ LOG.error("Failed to check status", e);
+ return KO;
+ }
+ }
+
+ private TransportClient getTransportClient() {
+ TransportClient res = this.transportClient.get();
+ if (res == null) {
+ res = buildTransportClient();
+ if (this.transportClient.compareAndSet(null, res)) {
+ return res;
+ }
+ return this.transportClient.get();
+ }
+ return res;
+ }
+
+ private TransportClient buildTransportClient() {
+ Settings.Builder esSettings = Settings.builder();
+
+ // mandatory property defined by bootstrap process
+ esSettings.put("cluster.name", esCommand.getClusterName());
+
+ TransportClient nativeClient = new MinimalTransportClient(esSettings.build());
+ HostAndPort host = HostAndPort.fromParts(esCommand.getHost(), esCommand.getPort());
+ addHostToClient(host, nativeClient);
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Connected to Elasticsearch node: [{}]", displayedAddresses(nativeClient));
+ }
+ return nativeClient;
+ }
+
+ private static void addHostToClient(HostAndPort host, TransportClient client) {
+ try {
+ client.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(host.getHostText()), host.getPortOrDefault(9001)));
+ } catch (UnknownHostException e) {
+ throw new IllegalStateException("Can not resolve host [" + host + "]", e);
+ }
+ }
+
+ private static String displayedAddresses(TransportClient nativeClient) {
+ return nativeClient.transportAddresses().stream().map(TransportAddress::toString).collect(Collectors.joining(", "));
+ }
+
+ enum Status {
+ CONNECTION_REFUSED, KO, RED, YELLOW, GREEN
+ }
+
+ @Override
+ public void askForStop() {
+ process.destroy();
+ }
+
+ @Override
+ public boolean askedForRestart() {
+ // ES does not support asking for restart
+ return false;
+ }
+
+ @Override
+ public void acknowledgeAskForRestart() {
+ // nothing to do
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumMap;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.ProcessId;
+
+import static org.sonar.application.process.Lifecycle.State.INIT;
+import static org.sonar.application.process.Lifecycle.State.STARTED;
+import static org.sonar.application.process.Lifecycle.State.STARTING;
+import static org.sonar.application.process.Lifecycle.State.STOPPED;
+import static org.sonar.application.process.Lifecycle.State.STOPPING;
+
+public class Lifecycle {
+
+ public enum State {
+ INIT, STARTING, STARTED, STOPPING, STOPPED
+ }
+
+ private static final Logger LOG = LoggerFactory.getLogger(Lifecycle.class);
+ private static final Map<State, Set<State>> TRANSITIONS = buildTransitions();
+
+ private final ProcessId processId;
+ private final List<ProcessLifecycleListener> listeners;
+ private State state;
+
+ public Lifecycle(ProcessId processId, List<ProcessLifecycleListener> listeners) {
+ this(processId, listeners, INIT);
+ }
+
+ Lifecycle(ProcessId processId, List<ProcessLifecycleListener> listeners, State initialState) {
+ this.processId = processId;
+ this.listeners = listeners;
+ this.state = initialState;
+ }
+
+ private static Map<State, Set<State>> buildTransitions() {
+ Map<State, Set<State>> res = new EnumMap<>(State.class);
+ res.put(INIT, toSet(STARTING));
+ res.put(STARTING, toSet(STARTED, STOPPING, STOPPED));
+ res.put(STARTED, toSet(STOPPING, STOPPED));
+ res.put(STOPPING, toSet(STOPPED));
+ res.put(STOPPED, toSet());
+ return res;
+ }
+
+ private static Set<State> toSet(State... states) {
+ if (states.length == 0) {
+ return Collections.emptySet();
+ }
+ if (states.length == 1) {
+ return Collections.singleton(states[0]);
+ }
+ return EnumSet.copyOf(Arrays.asList(states));
+ }
+
+ State getState() {
+ return state;
+ }
+
+ synchronized boolean tryToMoveTo(State to) {
+ boolean res = false;
+ State currentState = state;
+ if (TRANSITIONS.get(currentState).contains(to)) {
+ this.state = to;
+ res = true;
+ listeners.forEach(listener -> listener.onProcessState(processId, to));
+ }
+ LOG.trace("tryToMoveTo {} from {} to {} => {}", processId.getKey(), currentState, to, res);
+ return res;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import org.sonar.process.ProcessId;
+import org.sonar.process.sharedmemoryfile.ProcessCommands;
+
+import static java.util.Objects.requireNonNull;
+
+class ProcessCommandsProcessMonitor extends AbstractProcessMonitor {
+
+ private final ProcessCommands commands;
+
+ ProcessCommandsProcessMonitor(Process process, ProcessId processId, ProcessCommands commands) {
+ super(process, processId);
+ this.commands = requireNonNull(commands, "commands can't be null");
+ }
+
+ /**
+ * Whether the process has set the operational flag (in ipc shared memory)
+ */
+ @Override
+ public boolean isOperational() {
+ return commands.isOperational();
+ }
+
+ /**
+ * Send request to gracefully stop to the process (via ipc shared memory)
+ */
+ @Override
+ public void askForStop() {
+ commands.askForStop();
+ }
+
+ /**
+ * Whether the process asked for a full restart (via ipc shared memory)
+ */
+ @Override
+ public boolean askedForRestart() {
+ return commands.askedForRestart();
+ }
+
+ /**
+ * Removes the flag in ipc shared memory so that next call to {@link #askedForRestart()}
+ * returns {@code false}, except if meanwhile process asks again for restart.
+ */
+ @Override
+ public void acknowledgeAskForRestart() {
+ commands.acknowledgeAskForRestart();
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import org.sonar.process.ProcessId;
+
+@FunctionalInterface
+public interface ProcessEventListener {
+
+ enum Type {
+ OPERATIONAL,
+ ASK_FOR_RESTART
+ }
+
+ /**
+ * This method is called when the process with the specified {@link ProcessId}
+ * sends the event through the ipc shared memory.
+ * Note that there can be a delay since the instant the process sets the flag
+ * (see {@link SQProcess#WATCHER_DELAY_MS}).
+ *
+ * Call blocks the process watcher. Implementations should be asynchronous and
+ * fork a new thread if call can be long.
+ */
+ void onProcessEvent(ProcessId processId, Type type);
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.Closeable;
+import org.sonar.process.command.EsCommand;
+import org.sonar.process.command.JavaCommand;
+
+public interface ProcessLauncher extends Closeable {
+
+ @Override
+ void close();
+
+ /**
+ * Launch an ES command.
+ *
+ * @throws IllegalStateException if an error occurs
+ */
+ ProcessMonitor launch(EsCommand esCommand);
+
+ /**
+ * Launch a Java command.
+ *
+ * @throws IllegalStateException if an error occurs
+ */
+ ProcessMonitor launch(JavaCommand javaCommand);
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.function.Supplier;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.ProcessId;
+import org.sonar.process.command.AbstractCommand;
+import org.sonar.process.command.EsCommand;
+import org.sonar.process.command.JavaCommand;
+import org.sonar.process.jmvoptions.JvmOptions;
+import org.sonar.process.sharedmemoryfile.AllProcessesCommands;
+import org.sonar.process.sharedmemoryfile.ProcessCommands;
+
+import static java.lang.String.format;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_INDEX;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_KEY;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_SHARED_PATH;
+import static org.sonar.process.ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT;
+
+public class ProcessLauncherImpl implements ProcessLauncher {
+ private static final Logger LOG = LoggerFactory.getLogger(ProcessLauncherImpl.class);
+
+ private final File tempDir;
+ private final AllProcessesCommands allProcessesCommands;
+ private final Supplier<ProcessBuilder> processBuilderSupplier;
+
+ public ProcessLauncherImpl(File tempDir) {
+ this(tempDir, new AllProcessesCommands(tempDir), JavaLangProcessBuilder::new);
+ }
+
+ ProcessLauncherImpl(File tempDir, AllProcessesCommands allProcessesCommands, Supplier<ProcessBuilder> processBuilderSupplier) {
+ this.tempDir = tempDir;
+ this.allProcessesCommands = allProcessesCommands;
+ this.processBuilderSupplier = processBuilderSupplier;
+ }
+
+ @Override
+ public void close() {
+ allProcessesCommands.close();
+ }
+
+ @Override
+ public ProcessMonitor launch(EsCommand esCommand) {
+ Process process = null;
+ ProcessId processId = esCommand.getProcessId();
+ try {
+ writeConfFiles(esCommand);
+ ProcessBuilder processBuilder = create(esCommand);
+ logLaunchedCommand(esCommand, processBuilder);
+
+ process = processBuilder.start();
+
+ return new EsProcessMonitor(process, processId, esCommand);
+ } catch (Exception e) {
+ // just in case
+ if (process != null) {
+ process.destroyForcibly();
+ }
+ throw new IllegalStateException(format("Fail to launch process [%s]", processId.getKey()), e);
+ }
+ }
+
+ private void writeConfFiles(EsCommand esCommand) {
+ File confDir = esCommand.getConfDir();
+ if (!confDir.exists() && !confDir.mkdirs()) {
+ String error = format("Failed to create temporary configuration directory [%s]", confDir.getAbsolutePath());
+ LOG.error(error);
+ throw new IllegalStateException(error);
+ }
+
+ try {
+ IOUtils.copy(getClass().getResourceAsStream("elasticsearch.yml"), new FileOutputStream(new File(confDir, "elasticsearch.yml")));
+ esCommand.getEsJvmOptions().writeToJvmOptionFile(new File(confDir, "jvm.options"));
+ esCommand.getLog4j2Properties().store(new FileOutputStream(new File(confDir, "log4j2.properties")), "log42 properties file for ES bundled in SonarQube");
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to write ES configuration files", e);
+ }
+ }
+
+ @Override
+ public ProcessMonitor launch(JavaCommand javaCommand) {
+ Process process = null;
+ ProcessId processId = javaCommand.getProcessId();
+ try {
+ ProcessCommands commands = allProcessesCommands.createAfterClean(processId.getIpcIndex());
+
+ ProcessBuilder processBuilder = create(javaCommand);
+ logLaunchedCommand(javaCommand, processBuilder);
+ process = processBuilder.start();
+ return new ProcessCommandsProcessMonitor(process, processId, commands);
+ } catch (Exception e) {
+ // just in case
+ if (process != null) {
+ process.destroyForcibly();
+ }
+ throw new IllegalStateException(format("Fail to launch process [%s]", processId.getKey()), e);
+ }
+ }
+
+ private static <T extends AbstractCommand> void logLaunchedCommand(AbstractCommand<T> command, ProcessBuilder processBuilder) {
+ if (LOG.isInfoEnabled()) {
+ LOG.info("Launch process[{}] from [{}]: {}",
+ command.getProcessId(),
+ command.getWorkDir().getAbsolutePath(),
+ String.join(" ", processBuilder.command()));
+ }
+ }
+
+ private ProcessBuilder create(EsCommand esCommand) {
+ List<String> commands = new ArrayList<>();
+ commands.add(esCommand.getExecutable().getAbsolutePath());
+ commands.addAll(esCommand.getEsOptions());
+
+ return create(esCommand, commands);
+ }
+
+ private <T extends JvmOptions> ProcessBuilder create(JavaCommand<T> javaCommand) {
+ List<String> commands = new ArrayList<>();
+ commands.add(buildJavaPath());
+ commands.addAll(javaCommand.getJvmOptions().getAll());
+ commands.addAll(buildClasspath(javaCommand));
+ commands.add(javaCommand.getClassName());
+ commands.add(buildPropertiesFile(javaCommand).getAbsolutePath());
+
+ return create(javaCommand, commands);
+ }
+
+ private ProcessBuilder create(AbstractCommand<?> javaCommand, List<String> commands) {
+ ProcessBuilder processBuilder = processBuilderSupplier.get();
+ processBuilder.command(commands);
+ processBuilder.directory(javaCommand.getWorkDir());
+ processBuilder.environment().putAll(javaCommand.getEnvVariables());
+ processBuilder.redirectErrorStream(true);
+ return processBuilder;
+ }
+
+ private static String buildJavaPath() {
+ String separator = System.getProperty("file.separator");
+ return new File(new File(System.getProperty("java.home")), "bin" + separator + "java").getAbsolutePath();
+ }
+
+ private static List<String> buildClasspath(JavaCommand javaCommand) {
+ String pathSeparator = System.getProperty("path.separator");
+ return Arrays.asList("-cp", String.join(pathSeparator, javaCommand.getClasspath()));
+ }
+
+ private File buildPropertiesFile(JavaCommand javaCommand) {
+ File propertiesFile = null;
+ try {
+ propertiesFile = File.createTempFile("sq-process", "properties", tempDir);
+ Properties props = new Properties();
+ props.putAll(javaCommand.getArguments());
+ props.setProperty(PROPERTY_PROCESS_KEY, javaCommand.getProcessId().getKey());
+ props.setProperty(PROPERTY_PROCESS_INDEX, Integer.toString(javaCommand.getProcessId().getIpcIndex()));
+ props.setProperty(PROPERTY_TERMINATION_TIMEOUT, "60000");
+ props.setProperty(PROPERTY_SHARED_PATH, tempDir.getAbsolutePath());
+ try (OutputStream out = new FileOutputStream(propertiesFile)) {
+ props.store(out, format("Temporary properties file for command [%s]", javaCommand.getProcessId().getKey()));
+ }
+ return propertiesFile;
+ } catch (Exception e) {
+ throw new IllegalStateException("Cannot write temporary settings to " + propertiesFile, e);
+ }
+ }
+
+ /**
+ * An interface of the methods of {@link java.lang.ProcessBuilder} that we use in {@link ProcessLauncherImpl}.
+ * <p>Allows testing creating processes without actualling creating them at OS level</p>
+ */
+ public interface ProcessBuilder {
+ List<String> command();
+
+ ProcessBuilder command(List<String> commands);
+
+ ProcessBuilder directory(File dir);
+
+ Map<String, String> environment();
+
+ ProcessBuilder redirectErrorStream(boolean b);
+
+ Process start() throws IOException;
+ }
+
+ private static class JavaLangProcessBuilder implements ProcessBuilder {
+ private final java.lang.ProcessBuilder builder = new java.lang.ProcessBuilder();
+
+ /**
+ * @see java.lang.ProcessBuilder#command()
+ */
+ @Override
+ public List<String> command() {
+ return builder.command();
+ }
+
+ /**
+ * @see java.lang.ProcessBuilder#command(List)
+ */
+ @Override
+ public ProcessBuilder command(List<String> commands) {
+ builder.command(commands);
+ return this;
+ }
+
+ /**
+ * @see java.lang.ProcessBuilder#directory(File)
+ */
+ @Override
+ public ProcessBuilder directory(File dir) {
+ builder.directory(dir);
+ return this;
+ }
+
+ /**
+ * @see java.lang.ProcessBuilder#environment()
+ */
+ @Override
+ public Map<String, String> environment() {
+ return builder.environment();
+ }
+
+ /**
+ * @see java.lang.ProcessBuilder#redirectErrorStream(boolean)
+ */
+ @Override
+ public ProcessBuilder redirectErrorStream(boolean b) {
+ builder.redirectErrorStream(b);
+ return this;
+ }
+
+ /**
+ * @see java.lang.ProcessBuilder#start()
+ */
+ @Override
+ public Process start() throws IOException {
+ return builder.start();
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import org.sonar.process.ProcessId;
+
+@FunctionalInterface
+public interface ProcessLifecycleListener {
+
+ /**
+ * This method is called when the state of the process with the specified {@link ProcessId}
+ * changes.
+ *
+ * Call blocks the process watcher. Implementations should be asynchronous and
+ * fork a new thread if call can be long.
+ */
+ void onProcessState(ProcessId processId, Lifecycle.State state);
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+
+public interface ProcessMonitor {
+
+ /**
+ * @see Process#getInputStream()
+ */
+ InputStream getInputStream();
+
+ /**
+ * @see Process#getErrorStream()
+ */
+ InputStream getErrorStream();
+
+ /**
+ * Closes the streams {@link Process#getInputStream()}, {@link Process#getOutputStream()}
+ * and {@link Process#getErrorStream()}.
+ *
+ * No exceptions are thrown in case of errors.
+ */
+ void closeStreams();
+
+ /**
+ * @see Process#isAlive()
+ */
+ boolean isAlive();
+
+ /**
+ * @see Process#destroyForcibly()
+ */
+ void destroyForcibly();
+
+ /**
+ * @see Process#waitFor()
+ */
+ void waitFor() throws InterruptedException;
+
+ /**
+ * @see Process#waitFor(long, TimeUnit)
+ */
+ void waitFor(long timeout, TimeUnit timeoutUnit) throws InterruptedException;
+
+ /**
+ * Whether the process has reach operational state after startup.
+ */
+ boolean isOperational();
+
+ /**
+ * Send request to gracefully stop to the process
+ */
+ void askForStop();
+
+ /**
+ * Whether the process asked for a full restart
+ */
+ boolean askedForRestart();
+
+ /**
+ * Sends a signal to the process to acknowledge that the parent process received the request to restart from the
+ * child process send via {@link #askedForRestart()}.
+ * <br/>
+ * Child process will typically stop sending the signal requesting restart from now on.
+ */
+ void acknowledgeAskForRestart();
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.process;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Supplier;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.ProcessId;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+
+public class SQProcess {
+
+ public static final long DEFAULT_WATCHER_DELAY_MS = 500L;
+ private static final Logger LOG = LoggerFactory.getLogger(SQProcess.class);
+
+ private final ProcessId processId;
+ private final Lifecycle lifecycle;
+ private final List<ProcessEventListener> eventListeners;
+ private final long watcherDelayMs;
+
+ private ProcessMonitor process;
+ private StreamGobbler stdOutGobbler;
+ private StreamGobbler stdErrGobbler;
+ private final StopWatcher stopWatcher;
+ private final EventWatcher eventWatcher;
+ // keep flag so that the operational event is sent only once
+ // to listeners
+ private final AtomicBoolean operational = new AtomicBoolean(false);
+
+ private SQProcess(Builder builder) {
+ this.processId = requireNonNull(builder.processId, "processId can't be null");
+ this.lifecycle = new Lifecycle(this.processId, builder.lifecycleListeners);
+ this.eventListeners = builder.eventListeners;
+ this.watcherDelayMs = builder.watcherDelayMs;
+ this.stopWatcher = new StopWatcher();
+ this.eventWatcher = new EventWatcher();
+ }
+
+ public boolean start(Supplier<ProcessMonitor> commandLauncher) {
+ if (!lifecycle.tryToMoveTo(Lifecycle.State.STARTING)) {
+ // has already been started
+ return false;
+ }
+ try {
+ this.process = commandLauncher.get();
+ } catch (RuntimeException e) {
+ LOG.error(format("Fail to launch process [%s]", processId.getKey()), e);
+ lifecycle.tryToMoveTo(Lifecycle.State.STOPPED);
+ throw e;
+ }
+ this.stdOutGobbler = new StreamGobbler(process.getInputStream(), processId.getKey());
+ this.stdOutGobbler.start();
+ this.stdErrGobbler = new StreamGobbler(process.getErrorStream(), processId.getKey());
+ this.stdErrGobbler.start();
+ this.stopWatcher.start();
+ this.eventWatcher.start();
+ // Could be improved by checking the status "up" in shared memory.
+ // Not a problem so far as this state is not used by listeners.
+ lifecycle.tryToMoveTo(Lifecycle.State.STARTED);
+ return true;
+ }
+
+ public ProcessId getProcessId() {
+ return processId;
+ }
+
+ Lifecycle.State getState() {
+ return lifecycle.getState();
+ }
+
+ /**
+ * Sends kill signal and awaits termination. No guarantee that process is gracefully terminated (=shutdown hooks
+ * executed). It depends on OS.
+ */
+ public void stop(long timeout, TimeUnit timeoutUnit) {
+ if (lifecycle.tryToMoveTo(Lifecycle.State.STOPPING)) {
+ stopGracefully(timeout, timeoutUnit);
+ if (process != null && process.isAlive()) {
+ LOG.info("{} failed to stop in a timely fashion. Killing it.", processId.getKey());
+ }
+ // enforce stop and clean-up even if process has been gracefully stopped
+ stopForcibly();
+ } else {
+ // already stopping or stopped
+ waitForDown();
+ }
+ }
+
+ private void waitForDown() {
+ while (process != null && process.isAlive()) {
+ try {
+ process.waitFor();
+ } catch (InterruptedException ignored) {
+ // ignore, waiting for process to stop
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+
+ private void stopGracefully(long timeout, TimeUnit timeoutUnit) {
+ if (process == null) {
+ return;
+ }
+ try {
+ // request graceful stop
+ process.askForStop();
+ process.waitFor(timeout, timeoutUnit);
+ } catch (InterruptedException e) {
+ // can't wait for the termination of process. Let's assume it's down.
+ LOG.warn(format("Interrupted while stopping process %s", processId), e);
+ Thread.currentThread().interrupt();
+ } catch (Throwable e) {
+ LOG.error("Can not ask for graceful stop of process " + processId, e);
+ }
+ }
+
+ public void stopForcibly() {
+ eventWatcher.interrupt();
+ stopWatcher.interrupt();
+ if (process != null) {
+ process.destroyForcibly();
+ waitForDown();
+ process.closeStreams();
+ }
+ if (stdOutGobbler != null) {
+ StreamGobbler.waitUntilFinish(stdOutGobbler);
+ stdOutGobbler.interrupt();
+ }
+ if (stdErrGobbler != null) {
+ StreamGobbler.waitUntilFinish(stdErrGobbler);
+ stdErrGobbler.interrupt();
+ }
+ lifecycle.tryToMoveTo(Lifecycle.State.STOPPED);
+ }
+
+ void refreshState() {
+ if (process.isAlive()) {
+ if (!operational.get() && process.isOperational()) {
+ operational.set(true);
+ eventListeners.forEach(l -> l.onProcessEvent(processId, ProcessEventListener.Type.OPERATIONAL));
+ }
+ if (process.askedForRestart()) {
+ process.acknowledgeAskForRestart();
+ eventListeners.forEach(l -> l.onProcessEvent(processId, ProcessEventListener.Type.ASK_FOR_RESTART));
+ }
+ } else {
+ stopForcibly();
+ }
+ }
+
+ @Override
+ public String toString() {
+ return format("Process[%s]", processId.getKey());
+ }
+
+ /**
+ * This thread blocks as long as the monitored process is physically alive.
+ * It avoids from executing {@link Process#exitValue()} at a fixed rate :
+ * <ul>
+ * <li>no usage of exception for flow control. Indeed {@link Process#exitValue()} throws an exception
+ * if process is alive. There's no method <code>Process#isAlive()</code></li>
+ * <li>no delay, instantaneous notification that process is down</li>
+ * </ul>
+ */
+ private class StopWatcher extends Thread {
+ StopWatcher() {
+ // this name is different than Thread#toString(), which includes name, priority
+ // and thread group
+ // -> do not override toString()
+ super(format("StopWatcher[%s]", processId.getKey()));
+ }
+
+ @Override
+ public void run() {
+ try {
+ process.waitFor();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ // stop watching process
+ }
+ stopForcibly();
+ }
+ }
+
+ private class EventWatcher extends Thread {
+ EventWatcher() {
+ // this name is different than Thread#toString(), which includes name, priority
+ // and thread group
+ // -> do not override toString()
+ super(format("EventWatcher[%s]", processId.getKey()));
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (process.isAlive()) {
+ refreshState();
+ Thread.sleep(watcherDelayMs);
+ }
+ } catch (InterruptedException e) {
+ // request to stop watching process. To avoid unexpected behaviors
+ // the process is stopped.
+ Thread.currentThread().interrupt();
+ stopForcibly();
+ }
+ }
+ }
+
+ public static Builder builder(ProcessId processId) {
+ return new Builder(processId);
+ }
+
+ public static class Builder {
+ private final ProcessId processId;
+ private final List<ProcessEventListener> eventListeners = new ArrayList<>();
+ private final List<ProcessLifecycleListener> lifecycleListeners = new ArrayList<>();
+ private long watcherDelayMs = DEFAULT_WATCHER_DELAY_MS;
+
+ private Builder(ProcessId processId) {
+ this.processId = processId;
+ }
+
+ public Builder addEventListener(ProcessEventListener listener) {
+ this.eventListeners.add(listener);
+ return this;
+ }
+
+ public Builder addProcessLifecycleListener(ProcessLifecycleListener listener) {
+ this.lifecycleListeners.add(listener);
+ return this;
+ }
+
+ /**
+ * Default delay is {@link #DEFAULT_WATCHER_DELAY_MS}
+ */
+ public Builder setWatcherDelayMs(long l) {
+ this.watcherDelayMs = l;
+ return this;
+ }
+
+ public SQProcess build() {
+ return new SQProcess(this);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+/**
+ * Background thread that checks if a stop request
+ * is sent, usually by Orchestrator
+ */
+public interface StopRequestWatcher {
+
+ void startWatching();
+
+ void stopWatching();
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import org.sonar.application.FileSystem;
+import org.sonar.application.Scheduler;
+import org.sonar.application.config.AppSettings;
+import org.sonar.process.sharedmemoryfile.DefaultProcessCommands;
+import org.sonar.process.sharedmemoryfile.ProcessCommands;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+
+public class StopRequestWatcherImpl extends Thread implements StopRequestWatcher {
+
+ private static final long DEFAULT_WATCHER_DELAY_MS = 500L;
+
+ private final ProcessCommands commands;
+ private final Scheduler scheduler;
+ private final AppSettings settings;
+ private long delayMs = DEFAULT_WATCHER_DELAY_MS;
+
+ StopRequestWatcherImpl(AppSettings settings, Scheduler scheduler, ProcessCommands commands) {
+ super("StopRequestWatcherImpl");
+ this.settings = settings;
+ this.commands = commands;
+ this.scheduler = scheduler;
+
+ // safeguard, do not block the JVM if thread is not interrupted
+ // (method stopWatching() never called).
+ setDaemon(true);
+ }
+
+ public static StopRequestWatcherImpl create(AppSettings settings, Scheduler scheduler, FileSystem fs) {
+ DefaultProcessCommands commands = DefaultProcessCommands.secondary(fs.getTempDir(), ProcessId.APP.getIpcIndex());
+ return new StopRequestWatcherImpl(settings, scheduler, commands);
+ }
+
+ long getDelayMs() {
+ return delayMs;
+ }
+
+ void setDelayMs(long delayMs) {
+ this.delayMs = delayMs;
+ }
+
+ @Override
+ public void run() {
+ try {
+ while (true) {
+ if (commands.askedForStop()) {
+ scheduler.terminate();
+ return;
+ }
+ Thread.sleep(delayMs);
+ }
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ // stop watching the commands
+ }
+ }
+
+ @Override
+ public void startWatching() {
+ if (settings.getProps().valueAsBoolean(ProcessProperties.ENABLE_STOP_COMMAND)) {
+ start();
+ }
+ }
+
+ @Override
+ public void stopWatching() {
+ // does nothing is not started
+ interrupt();
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import javax.annotation.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+/**
+ * Reads process output and writes to logs
+ */
+public class StreamGobbler extends Thread {
+
+ public static final String LOGGER_GOBBLER = "gobbler";
+
+ private final InputStream is;
+ private final Logger logger;
+
+ StreamGobbler(InputStream is, String processKey) {
+ this(is, processKey, LoggerFactory.getLogger(LOGGER_GOBBLER));
+ }
+
+ StreamGobbler(InputStream is, String processKey, Logger logger) {
+ super(String.format("Gobbler[%s]", processKey));
+ this.is = is;
+ this.logger = logger;
+ }
+
+ @Override
+ public void run() {
+ try (BufferedReader br = new BufferedReader(new InputStreamReader(is, UTF_8))) {
+ String line;
+ while ((line = br.readLine()) != null) {
+ logger.info(line);
+ }
+ } catch (Exception ignored) {
+ // ignored
+ }
+ }
+
+ static void waitUntilFinish(@Nullable StreamGobbler gobbler) {
+ if (gobbler != null) {
+ try {
+ gobbler.join();
+ } catch (InterruptedException ignored) {
+ // consider as finished, restore the interrupted flag
+ Thread.currentThread().interrupt();
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.application.process;
+
+import javax.annotation.ParametersAreNonnullByDefault;
--- /dev/null
+# ======================== Elasticsearch Configuration =========================
+#
+# NOTE: Elasticsearch comes with reasonable defaults for most settings.
+# Before you set out to tweak and tune the configuration, make sure you
+# understand what are you trying to accomplish and the consequences.
+#
+# The primary way of configuring a node is via this file. This template lists
+# the most important settings you may want to configure for a production cluster.
+#
+# Please see the documentation for further information on configuration options:
+# <https://www.elastic.co/guide/en/elasticsearch/reference/5.0/settings.html>
+#
+# ---------------------------------- Cluster -----------------------------------
+#
+# Use a descriptive name for your cluster:
+#
+#cluster.name: my-application
+#
+# ------------------------------------ Node ------------------------------------
+#
+# Use a descriptive name for the node:
+#
+#node.name: node-1
+#
+# Add custom attributes to the node:
+#
+#node.attr.rack: r1
+#
+# ----------------------------------- Paths ------------------------------------
+#
+# Path to directory where to store the data (separate multiple locations by comma):
+#
+#path.data: /path/to/data
+#
+# Path to log files:
+#
+#path.logs: /path/to/logs
+#
+# ----------------------------------- Memory -----------------------------------
+#
+# Lock the memory on startup:
+#
+#bootstrap.memory_lock: true
+#
+# Make sure that the heap size is set to about half the memory available
+# on the system and that the owner of the process is allowed to use this
+# limit.
+#
+# Elasticsearch performs poorly when the system is swapping the memory.
+#
+# ---------------------------------- Network -----------------------------------
+#
+# Set the bind address to a specific IP (IPv4 or IPv6):
+#
+#network.host: 192.168.0.1
+#
+# Set a custom port for HTTP:
+#
+#http.port: 9200
+#
+# For more information, see the documentation at:
+# <https://www.elastic.co/guide/en/elasticsearch/reference/5.0/modules-network.html>
+#
+# --------------------------------- Discovery ----------------------------------
+#
+# Pass an initial list of hosts to perform discovery when new node is started:
+# The default list of hosts is ["127.0.0.1", "[::1]"]
+#
+#discovery.zen.ping.unicast.hosts: ["host1", "host2"]
+#
+# Prevent the "split brain" by configuring the majority of nodes (total number of nodes / 2 + 1):
+#
+#discovery.zen.minimum_master_nodes: 3
+#
+# For more information, see the documentation at:
+# <https://www.elastic.co/guide/en/elasticsearch/reference/5.0/modules-discovery-zen.html>
+#
+# ---------------------------------- Gateway -----------------------------------
+#
+# Block initial recovery after a full cluster restart until N nodes are started:
+#
+#gateway.recover_after_nodes: 3
+#
+# For more information, see the documentation at:
+# <https://www.elastic.co/guide/en/elasticsearch/reference/5.0/modules-gateway.html>
+#
+# ---------------------------------- Various -----------------------------------
+#
+# Require explicit names when deleting indices:
+#
+#action.destructive_requires_name: true
--- /dev/null
+${buildVersion}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.attribute.BasicFileAttributes;
+import javax.annotation.CheckForNull;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.sharedmemoryfile.AllProcessesCommands;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.process.sharedmemoryfile.ProcessCommands.MAX_PROCESSES;
+
+public class AppFileSystemTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private File homeDir;
+ private File dataDir;
+ private File tempDir;
+ private File logsDir;
+ private File webDir;
+ private TestAppSettings settings = new TestAppSettings();
+ private AppFileSystem underTest = new AppFileSystem(settings);
+
+ @Before
+ public void before() throws IOException {
+ homeDir = temp.newFolder();
+ dataDir = new File(homeDir, "data");
+ tempDir = new File(homeDir, "temp");
+ logsDir = new File(homeDir, "logs");
+ webDir = new File(homeDir, "web");
+
+ settings.getProps().set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+ settings.getProps().set(ProcessProperties.PATH_DATA, dataDir.getAbsolutePath());
+ settings.getProps().set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath());
+ settings.getProps().set(ProcessProperties.PATH_LOGS, logsDir.getAbsolutePath());
+ settings.getProps().set(ProcessProperties.PATH_WEB, webDir.getAbsolutePath());
+ }
+
+ @Test
+ public void reset_creates_dirs_if_they_don_t_exist() throws Exception {
+ assertThat(dataDir).doesNotExist();
+
+ underTest.reset();
+
+ assertThat(dataDir).exists().isDirectory();
+ assertThat(logsDir).exists().isDirectory();
+ assertThat(tempDir).exists().isDirectory();
+ assertThat(webDir).exists().isDirectory();
+
+ underTest.reset();
+
+ assertThat(dataDir).exists().isDirectory();
+ assertThat(logsDir).exists().isDirectory();
+ assertThat(tempDir).exists().isDirectory();
+ assertThat(webDir).exists().isDirectory();
+ }
+
+ @Test
+ public void reset_deletes_content_of_temp_dir_but_not_temp_dir_itself_if_it_already_exists() throws Exception {
+ assertThat(tempDir.mkdir()).isTrue();
+ Object tempDirKey = getFileKey(tempDir);
+ File fileInTempDir = new File(tempDir, "someFile.txt");
+ assertThat(fileInTempDir.createNewFile()).isTrue();
+ File subDirInTempDir = new File(tempDir, "subDir");
+ assertThat(subDirInTempDir.mkdir()).isTrue();
+
+ underTest.reset();
+
+ assertThat(tempDir).exists();
+ assertThat(fileInTempDir).doesNotExist();
+ assertThat(subDirInTempDir).doesNotExist();
+ assertThat(getFileKey(tempDir)).isEqualTo(tempDirKey);
+ }
+
+ @Test
+ public void reset_deletes_content_of_temp_dir_but_not_sharedmemory_file() throws Exception {
+ assertThat(tempDir.mkdir()).isTrue();
+ File sharedmemory = new File(tempDir, "sharedmemory");
+ assertThat(sharedmemory.createNewFile()).isTrue();
+ FileUtils.write(sharedmemory, "toto");
+ Object fileKey = getFileKey(sharedmemory);
+
+ Object tempDirKey = getFileKey(tempDir);
+ File fileInTempDir = new File(tempDir, "someFile.txt");
+ assertThat(fileInTempDir.createNewFile()).isTrue();
+
+ underTest.reset();
+
+ assertThat(tempDir).exists();
+ assertThat(fileInTempDir).doesNotExist();
+ assertThat(getFileKey(tempDir)).isEqualTo(tempDirKey);
+ assertThat(getFileKey(sharedmemory)).isEqualTo(fileKey);
+ // content of sharedMemory file is reset
+ assertThat(FileUtils.readFileToString(sharedmemory)).isNotEqualTo("toto");
+ }
+
+ @Test
+ public void reset_cleans_the_sharedmemory_file() throws IOException {
+ assertThat(tempDir.mkdir()).isTrue();
+ try (AllProcessesCommands commands = new AllProcessesCommands(tempDir)) {
+ for (int i = 0; i < MAX_PROCESSES; i++) {
+ commands.create(i).setUp();
+ }
+
+ underTest.reset();
+
+ for (int i = 0; i < MAX_PROCESSES; i++) {
+ assertThat(commands.create(i).isUp()).isFalse();
+ }
+ }
+ }
+
+ @CheckForNull
+ private static Object getFileKey(File fileInTempDir) throws IOException {
+ Path path = Paths.get(fileInTempDir.toURI());
+ BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
+ return attrs.fileKey();
+ }
+
+ @Test
+ public void reset_throws_ISE_if_data_dir_is_a_file() throws Exception {
+ resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_DATA);
+ }
+
+ @Test
+ public void reset_throws_ISE_if_web_dir_is_a_file() throws Exception {
+ resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_WEB);
+ }
+
+ @Test
+ public void reset_throws_ISE_if_logs_dir_is_a_file() throws Exception {
+ resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_LOGS);
+ }
+
+ @Test
+ public void reset_throws_ISE_if_temp_dir_is_a_file() throws Exception {
+ resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_TEMP);
+ }
+
+ private void resetThrowsISEIfDirIsAFile(String property) throws IOException {
+ File file = new File(homeDir, "zoom.store");
+ assertThat(file.createNewFile()).isTrue();
+ settings.getProps().set(property, file.getAbsolutePath());
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Property '" + property + "' is not valid, not a directory: " + file.getAbsolutePath());
+
+ underTest.reset();
+ }
+
+ @Test
+ public void fail_if_required_directory_is_a_file() throws Exception {
+ // <home>/data is missing
+ FileUtils.forceMkdir(webDir);
+ FileUtils.forceMkdir(logsDir);
+ FileUtils.touch(dataDir);
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Property 'sonar.path.data' is not valid, not a directory: " + dataDir.getAbsolutePath());
+
+ underTest.reset();
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.Appender;
+import ch.qos.logback.core.ConsoleAppender;
+import ch.qos.logback.core.FileAppender;
+import ch.qos.logback.core.encoder.Encoder;
+import ch.qos.logback.core.joran.spi.ConsoleTarget;
+import ch.qos.logback.core.rolling.RollingFileAppender;
+import java.io.File;
+import java.io.IOException;
+import java.util.Iterator;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.slf4j.LoggerFactory;
+import org.sonar.application.config.AppSettings;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.logging.LogbackHelper;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.slf4j.Logger.ROOT_LOGGER_NAME;
+import static org.sonar.application.process.StreamGobbler.LOGGER_GOBBLER;
+
+public class AppLoggingTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private File logDir;
+
+ private AppSettings settings = new TestAppSettings();
+ private AppLogging underTest = new AppLogging(settings);
+
+ @Before
+ public void setUp() throws Exception {
+ logDir = temp.newFolder();
+ settings.getProps().set(ProcessProperties.PATH_LOGS, logDir.getAbsolutePath());
+ }
+
+ @AfterClass
+ public static void resetLogback() throws Exception {
+ new LogbackHelper().resetFromXml("/logback-test.xml");
+ }
+
+ @Test
+ public void no_writing_to_sonar_log_file_when_running_from_sonar_script() {
+ emulateRunFromSonarScript();
+
+ LoggerContext ctx = underTest.configure();
+
+ ctx.getLoggerList().forEach(AppLoggingTest::verifyNoFileAppender);
+ }
+
+ @Test
+ public void root_logger_only_writes_to_console_with_formatting_when_running_from_sonar_script() {
+ emulateRunFromSonarScript();
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) rootLogger.getAppender("APP_CONSOLE");
+ verifyAppFormattedLogEncoder(consoleAppender.getEncoder());
+ assertThat(rootLogger.iteratorForAppenders()).hasSize(1);
+ }
+
+ @Test
+ public void gobbler_logger_writes_to_console_without_formatting_when_running_from_sonar_script() {
+ emulateRunFromSonarScript();
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
+ verifyGobblerConsoleAppender(gobblerLogger);
+ assertThat(gobblerLogger.iteratorForAppenders()).hasSize(1);
+ }
+
+ @Test
+ public void root_logger_writes_to_console_with_formatting_and_to_sonar_log_file_when_running_from_command_line() {
+ emulateRunFromCommandLine(false);
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ verifyAppConsoleAppender(rootLogger.getAppender("APP_CONSOLE"));
+ verifySonarLogFileAppender(rootLogger.getAppender("file_sonar"));
+ assertThat(rootLogger.iteratorForAppenders()).hasSize(2);
+
+ // verify no other logger writes to sonar.log
+ ctx.getLoggerList()
+ .stream()
+ .filter(logger -> !ROOT_LOGGER_NAME.equals(logger.getName()))
+ .forEach(AppLoggingTest::verifyNoFileAppender);
+ }
+
+ @Test
+ public void gobbler_logger_writes_to_console_without_formatting_when_running_from_command_line() {
+ emulateRunFromCommandLine(false);
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
+ verifyGobblerConsoleAppender(gobblerLogger);
+ assertThat(gobblerLogger.iteratorForAppenders()).hasSize(1);
+ }
+
+ @Test
+ public void root_logger_writes_to_console_with_formatting_and_to_sonar_log_file_when_running_from_ITs() {
+ emulateRunFromCommandLine(true);
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ verifyAppConsoleAppender(rootLogger.getAppender("APP_CONSOLE"));
+ verifySonarLogFileAppender(rootLogger.getAppender("file_sonar"));
+ assertThat(rootLogger.iteratorForAppenders()).hasSize(2);
+
+ ctx.getLoggerList()
+ .stream()
+ .filter(logger -> !ROOT_LOGGER_NAME.equals(logger.getName()))
+ .forEach(AppLoggingTest::verifyNoFileAppender);
+ }
+
+ @Test
+ public void gobbler_logger_writes_to_console_without_formatting_when_running_from_ITs() {
+ emulateRunFromCommandLine(true);
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
+ verifyGobblerConsoleAppender(gobblerLogger);
+ assertThat(gobblerLogger.iteratorForAppenders()).hasSize(1);
+ }
+
+ @Test
+ public void configure_no_rotation_on_sonar_file() {
+ settings.getProps().set("sonar.log.rollingPolicy", "none");
+
+ LoggerContext ctx = underTest.configure();
+
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ Appender<ILoggingEvent> appender = rootLogger.getAppender("file_sonar");
+ assertThat(appender)
+ .isNotInstanceOf(RollingFileAppender.class)
+ .isInstanceOf(FileAppender.class);
+ }
+
+ @Test
+ public void default_level_for_root_logger_is_INFO() {
+ LoggerContext ctx = underTest.configure();
+
+ verifyRootLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void root_logger_level_changes_with_global_property() {
+ settings.getProps().set("sonar.log.level", "TRACE");
+
+ LoggerContext ctx = underTest.configure();
+
+ verifyRootLogLevel(ctx, Level.TRACE);
+ }
+
+ @Test
+ public void root_logger_level_changes_with_app_property() {
+ settings.getProps().set("sonar.log.level.app", "TRACE");
+
+ LoggerContext ctx = underTest.configure();
+
+ verifyRootLogLevel(ctx, Level.TRACE);
+ }
+
+ @Test
+ public void root_logger_level_is_configured_from_app_property_over_global_property() {
+ settings.getProps().set("sonar.log.level", "TRACE");
+ settings.getProps().set("sonar.log.level.app", "DEBUG");
+
+ LoggerContext ctx = underTest.configure();
+
+ verifyRootLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void root_logger_level_changes_with_app_property_and_is_case_insensitive() {
+ settings.getProps().set("sonar.log.level.app", "debug");
+
+ LoggerContext ctx = underTest.configure();
+
+ verifyRootLogLevel(ctx, Level.DEBUG);
+ }
+
+ @Test
+ public void default_to_INFO_if_app_property_has_invalid_value() {
+ settings.getProps().set("sonar.log.level.app", "DodoDouh!");
+
+ LoggerContext ctx = underTest.configure();
+ verifyRootLogLevel(ctx, Level.INFO);
+ }
+
+ @Test
+ public void fail_with_IAE_if_global_property_unsupported_level() {
+ settings.getProps().set("sonar.log.level", "ERROR");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("log level ERROR in property sonar.log.level is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
+
+ underTest.configure();
+ }
+
+ @Test
+ public void fail_with_IAE_if_app_property_unsupported_level() {
+ settings.getProps().set("sonar.log.level.app", "ERROR");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("log level ERROR in property sonar.log.level.app is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
+
+ underTest.configure();
+ }
+
+ @Test
+ public void no_info_log_from_hazelcast() throws IOException {
+ settings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ underTest.configure();
+
+ assertThat(
+ LoggerFactory.getLogger("com.hazelcast").isInfoEnabled()).isEqualTo(false);
+ }
+
+ @Test
+ public void configure_logging_for_hazelcast() throws IOException {
+ settings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.getProps().set(ProcessProperties.HAZELCAST_LOG_LEVEL, "INFO");
+ underTest.configure();
+
+ assertThat(
+ LoggerFactory.getLogger("com.hazelcast").isInfoEnabled()).isEqualTo(true);
+ assertThat(
+ LoggerFactory.getLogger("com.hazelcast").isDebugEnabled()).isEqualTo(false);
+ }
+
+ private void emulateRunFromSonarScript() {
+ settings.getProps().set("sonar.wrapped", "true");
+ }
+
+ private void emulateRunFromCommandLine(boolean withAllLogsPrintedToConsole) {
+ if (withAllLogsPrintedToConsole) {
+ settings.getProps().set("sonar.log.console", "true");
+ }
+ }
+
+ private static void verifyNoFileAppender(Logger logger) {
+ Iterator<Appender<ILoggingEvent>> iterator = logger.iteratorForAppenders();
+ while (iterator.hasNext()) {
+ assertThat(iterator.next()).isNotInstanceOf(FileAppender.class);
+ }
+ }
+
+ private void verifySonarLogFileAppender(Appender<ILoggingEvent> appender) {
+ assertThat(appender).isInstanceOf(FileAppender.class);
+ FileAppender fileAppender = (FileAppender) appender;
+ assertThat(fileAppender.getFile()).isEqualTo(new File(logDir, "sonar.log").getAbsolutePath());
+ verifyAppFormattedLogEncoder(fileAppender.getEncoder());
+ }
+
+ private void verifyAppConsoleAppender(Appender<ILoggingEvent> appender) {
+ assertThat(appender).isInstanceOf(ConsoleAppender.class);
+ ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) appender;
+ assertThat(consoleAppender.getTarget()).isEqualTo(ConsoleTarget.SystemOut.getName());
+ verifyAppFormattedLogEncoder(consoleAppender.getEncoder());
+ }
+
+ private void verifyAppFormattedLogEncoder(Encoder<ILoggingEvent> encoder) {
+ verifyFormattedLogEncoder(encoder, "%d{yyyy.MM.dd HH:mm:ss} %-5level app[][%logger{20}] %msg%n");
+ }
+
+ private void verifyGobblerConsoleAppender(Logger logger) {
+ Appender<ILoggingEvent> appender = logger.getAppender("GOBBLER_CONSOLE");
+ assertThat(appender).isInstanceOf(ConsoleAppender.class);
+ ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) appender;
+ assertThat(consoleAppender.getTarget()).isEqualTo(ConsoleTarget.SystemOut.getName());
+ verifyFormattedLogEncoder(consoleAppender.getEncoder(), "%msg%n");
+ }
+
+ private void verifyFormattedLogEncoder(Encoder<ILoggingEvent> encoder, String logPattern) {
+ assertThat(encoder).isInstanceOf(PatternLayoutEncoder.class);
+ PatternLayoutEncoder patternEncoder = (PatternLayoutEncoder) encoder;
+ assertThat(patternEncoder.getPattern()).isEqualTo(logPattern);
+ }
+
+ private void verifyRootLogLevel(LoggerContext ctx, Level expected) {
+ Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
+ assertThat(rootLogger.getLevel()).isEqualTo(expected);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.application.config.AppSettings;
+import org.sonar.application.config.AppSettingsLoader;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.MessageException;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.data.MapEntry.entry;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class AppReloaderImplTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private AppSettingsLoader settingsLoader = mock(AppSettingsLoader.class);
+ private FileSystem fs = mock(FileSystem.class);
+ private AppState state = mock(AppState.class);
+ private AppLogging logging = mock(AppLogging.class);
+ private AppReloaderImpl underTest = new AppReloaderImpl(settingsLoader, fs, state, logging);
+
+ @Test
+ public void reload_configuration_then_reset_all() throws IOException {
+ AppSettings settings = new TestAppSettings().set("foo", "bar");
+ AppSettings newSettings = new TestAppSettings()
+ .set("foo", "newBar")
+ .set("newProp", "newVal");
+ when(settingsLoader.load()).thenReturn(newSettings);
+
+ underTest.reload(settings);
+
+ assertThat(settings.getProps().rawProperties())
+ .contains(entry("foo", "newBar"))
+ .contains(entry("newProp", "newVal"));
+ verify(logging).configure();
+ verify(state).reset();
+ verify(fs).reset();
+ }
+
+ @Test
+ public void throw_ISE_if_cluster_is_enabled() throws IOException {
+ AppSettings settings = new TestAppSettings().set(ProcessProperties.CLUSTER_ENABLED, "true");
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Restart is not possible with cluster mode");
+
+ underTest.reload(settings);
+
+ verifyZeroInteractions(logging);
+ verifyZeroInteractions(state);
+ verifyZeroInteractions(fs);
+ }
+
+ @Test
+ public void throw_MessageException_if_path_properties_are_changed() throws IOException {
+ verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_DATA);
+ verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_LOGS);
+ verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_TEMP);
+ verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_WEB);
+ }
+
+ @Test
+ public void throw_MessageException_if_cluster_mode_changed() throws IOException {
+ verifyFailureIfPropertyValueChanged(ProcessProperties.CLUSTER_ENABLED);
+ }
+
+ private void verifyFailureIfPropertyValueChanged(String propertyKey) throws IOException {
+ AppSettings settings = new TestAppSettings().set(propertyKey, "val1");
+ AppSettings newSettings = new TestAppSettings()
+ .set(propertyKey, "val2");
+ when(settingsLoader.load()).thenReturn(newSettings);
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Property [" + propertyKey + "] cannot be changed on restart: [val1] => [val2]");
+
+ underTest.reload(settings);
+
+ verifyZeroInteractions(logging);
+ verifyZeroInteractions(state);
+ verifyZeroInteractions(fs);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import org.junit.Test;
+import org.sonar.application.cluster.AppStateClusterImpl;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AppStateFactoryTest {
+
+ private TestAppSettings settings = new TestAppSettings();
+ private AppStateFactory underTest = new AppStateFactory(settings);
+
+ @Test
+ public void create_cluster_implementation_if_cluster_is_enabled() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_NAME, "foo");
+
+ AppState appState = underTest.create();
+ assertThat(appState).isInstanceOf(AppStateClusterImpl.class);
+ ((AppStateClusterImpl) appState).close();
+ }
+
+ @Test
+ public void cluster_implementation_is_disabled_by_default() {
+ assertThat(underTest.create()).isInstanceOf(AppStateImpl.class);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import org.junit.Test;
+import org.sonar.process.ProcessId;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+public class AppStateImplTest {
+
+ private AppStateListener listener = mock(AppStateListener.class);
+ private AppStateImpl underTest = new AppStateImpl();
+
+ @Test
+ public void get_and_set_operational_flag() {
+ assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isFalse();
+
+ underTest.setOperational(ProcessId.ELASTICSEARCH);
+
+ assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isTrue();
+ assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isFalse();
+
+ // only local mode is supported. App state = local state
+ assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, false)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, false)).isTrue();
+ assertThat(underTest.isOperational(ProcessId.WEB_SERVER, false)).isFalse();
+ }
+
+ @Test
+ public void notify_listeners_when_a_process_becomes_operational() {
+ underTest.addListener(listener);
+
+ underTest.setOperational(ProcessId.ELASTICSEARCH);
+
+ verify(listener).onAppStateOperational(ProcessId.ELASTICSEARCH);
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void tryToLockWebLeader_returns_true_if_first_call() {
+ assertThat(underTest.tryToLockWebLeader()).isTrue();
+
+ // next calls return false
+ assertThat(underTest.tryToLockWebLeader()).isFalse();
+ assertThat(underTest.tryToLockWebLeader()).isFalse();
+ }
+
+ @Test
+ public void reset_initializes_all_flags() {
+ underTest.setOperational(ProcessId.ELASTICSEARCH);
+ assertThat(underTest.tryToLockWebLeader()).isTrue();
+
+ underTest.reset();
+
+ assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isFalse();
+ assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isFalse();
+ assertThat(underTest.tryToLockWebLeader()).isTrue();
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.mockito.Mockito;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.application.process.ProcessLauncher;
+import org.sonar.application.process.ProcessMonitor;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.command.AbstractCommand;
+import org.sonar.process.command.CommandFactory;
+import org.sonar.process.command.EsCommand;
+import org.sonar.process.command.JavaCommand;
+
+import static java.util.Collections.synchronizedList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
+import static org.sonar.process.ProcessId.ELASTICSEARCH;
+import static org.sonar.process.ProcessId.WEB_SERVER;
+
+public class SchedulerImplTest {
+
+ @Rule
+ public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private EsCommand esCommand;
+ private JavaCommand webLeaderCommand;
+ private JavaCommand webFollowerCommand;
+ private JavaCommand ceCommand;
+
+ private AppReloader appReloader = mock(AppReloader.class);
+ private TestAppSettings settings = new TestAppSettings();
+ private TestCommandFactory javaCommandFactory = new TestCommandFactory();
+ private TestProcessLauncher processLauncher = new TestProcessLauncher();
+ private TestAppState appState = new TestAppState();
+ private List<ProcessId> orderedStops = synchronizedList(new ArrayList<>());
+
+ @Before
+ public void setUp() throws Exception {
+ File tempDir = temporaryFolder.newFolder();
+ esCommand = new EsCommand(ELASTICSEARCH, tempDir);
+ webLeaderCommand = new JavaCommand(WEB_SERVER, tempDir);
+ webFollowerCommand = new JavaCommand(WEB_SERVER, tempDir);
+ ceCommand = new JavaCommand(COMPUTE_ENGINE, tempDir);
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ processLauncher.close();
+ }
+
+ @Test
+ public void start_and_stop_sequence_of_ES_WEB_CE_in_order() throws Exception {
+ enableAllProcesses();
+ SchedulerImpl underTest = newScheduler();
+ underTest.schedule();
+
+ // elasticsearch does not have preconditions to start
+ TestProcess es = processLauncher.waitForProcess(ELASTICSEARCH);
+ assertThat(es.isAlive()).isTrue();
+ assertThat(processLauncher.processes).hasSize(1);
+
+ // elasticsearch becomes operational -> web leader is starting
+ es.operational = true;
+ waitForAppStateOperational(ELASTICSEARCH);
+ TestProcess web = processLauncher.waitForProcess(WEB_SERVER);
+ assertThat(web.isAlive()).isTrue();
+ assertThat(processLauncher.processes).hasSize(2);
+ assertThat(processLauncher.commands).containsExactly(esCommand, webLeaderCommand);
+
+ // web becomes operational -> CE is starting
+ web.operational = true;
+ waitForAppStateOperational(WEB_SERVER);
+ TestProcess ce = processLauncher.waitForProcess(COMPUTE_ENGINE);
+ assertThat(ce.isAlive()).isTrue();
+ assertThat(processLauncher.processes).hasSize(3);
+ assertThat(processLauncher.commands).containsExactly(esCommand, webLeaderCommand, ceCommand);
+
+ // all processes are up
+ processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isTrue());
+
+ // processes are stopped in reverse order of startup
+ underTest.terminate();
+ assertThat(orderedStops).containsExactly(COMPUTE_ENGINE, WEB_SERVER, ELASTICSEARCH);
+ processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
+
+ // does nothing because scheduler is already terminated
+ underTest.awaitTermination();
+ }
+
+ private void enableAllProcesses() {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ }
+
+ @Test
+ public void all_processes_are_stopped_if_one_process_goes_down() throws Exception {
+ Scheduler underTest = startAll();
+
+ processLauncher.waitForProcess(WEB_SERVER).destroyForcibly();
+
+ underTest.awaitTermination();
+ assertThat(orderedStops).containsExactly(WEB_SERVER, COMPUTE_ENGINE, ELASTICSEARCH);
+ processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
+
+ // following does nothing
+ underTest.terminate();
+ underTest.awaitTermination();
+ }
+
+ @Test
+ public void all_processes_are_stopped_if_one_process_fails_to_start() throws Exception {
+ enableAllProcesses();
+ SchedulerImpl underTest = newScheduler();
+ processLauncher.makeStartupFail = COMPUTE_ENGINE;
+
+ underTest.schedule();
+
+ processLauncher.waitForProcess(ELASTICSEARCH).operational = true;
+ processLauncher.waitForProcess(WEB_SERVER).operational = true;
+
+ underTest.awaitTermination();
+ assertThat(orderedStops).containsExactly(WEB_SERVER, ELASTICSEARCH);
+ processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
+ }
+
+ @Test
+ public void terminate_can_be_called_multiple_times() throws Exception {
+ Scheduler underTest = startAll();
+
+ underTest.terminate();
+ processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
+
+ // does nothing
+ underTest.terminate();
+ }
+
+ @Test
+ public void awaitTermination_blocks_until_all_processes_are_stopped() throws Exception {
+ Scheduler underTest = startAll();
+
+ Thread awaitingTermination = new Thread(() -> underTest.awaitTermination());
+ awaitingTermination.start();
+ assertThat(awaitingTermination.isAlive()).isTrue();
+
+ underTest.terminate();
+ // the thread is being stopped
+ awaitingTermination.join();
+ assertThat(awaitingTermination.isAlive()).isFalse();
+ }
+
+ @Test
+ @Ignore("false-positives on Travis CI")
+ public void restart_reloads_java_commands_and_restarts_all_processes() throws Exception {
+ Scheduler underTest = startAll();
+
+ processLauncher.waitForProcess(WEB_SERVER).askedForRestart = true;
+
+ // waiting for all processes to be stopped
+ boolean stopped = false;
+ while (!stopped) {
+ stopped = orderedStops.size() == 3;
+ Thread.sleep(1L);
+ }
+
+ // restarting
+ verify(appReloader, timeout(60_000)).reload(settings);
+ processLauncher.waitForProcessAlive(ELASTICSEARCH);
+ processLauncher.waitForProcessAlive(COMPUTE_ENGINE);
+ processLauncher.waitForProcessAlive(WEB_SERVER);
+
+ underTest.terminate();
+ // 3+3 processes have been stopped
+ assertThat(orderedStops).hasSize(6);
+ assertThat(processLauncher.waitForProcess(ELASTICSEARCH).isAlive()).isFalse();
+ assertThat(processLauncher.waitForProcess(COMPUTE_ENGINE).isAlive()).isFalse();
+ assertThat(processLauncher.waitForProcess(WEB_SERVER).isAlive()).isFalse();
+
+ // verify that awaitTermination() does not block
+ underTest.awaitTermination();
+ }
+
+ @Test
+ public void restart_stops_all_if_new_settings_are_not_allowed() throws Exception {
+ Scheduler underTest = startAll();
+ doThrow(new IllegalStateException("reload error")).when(appReloader).reload(settings);
+
+ processLauncher.waitForProcess(WEB_SERVER).askedForRestart = true;
+
+ // waiting for all processes to be stopped
+ processLauncher.waitForProcessDown(ELASTICSEARCH);
+ processLauncher.waitForProcessDown(COMPUTE_ENGINE);
+ processLauncher.waitForProcessDown(WEB_SERVER);
+
+ // verify that awaitTermination() does not block
+ underTest.awaitTermination();
+ }
+
+ @Test
+ public void web_follower_starts_only_when_web_leader_is_operational() throws Exception {
+ // leader takes the lock, so underTest won't get it
+ assertThat(appState.tryToLockWebLeader()).isTrue();
+
+ appState.setOperational(ProcessId.ELASTICSEARCH);
+ enableAllProcesses();
+ SchedulerImpl underTest = newScheduler();
+ underTest.schedule();
+
+ processLauncher.waitForProcessAlive(ProcessId.ELASTICSEARCH);
+ assertThat(processLauncher.processes).hasSize(1);
+
+ // leader becomes operational -> follower can start
+ appState.setOperational(ProcessId.WEB_SERVER);
+ processLauncher.waitForProcessAlive(WEB_SERVER);
+
+ underTest.terminate();
+ }
+
+ @Test
+ public void web_server_waits_for_remote_elasticsearch_to_be_started_if_local_es_is_disabled() throws Exception {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
+ SchedulerImpl underTest = newScheduler();
+ underTest.schedule();
+
+ // WEB and CE wait for ES to be up
+ assertThat(processLauncher.processes).isEmpty();
+
+ // ES becomes operational on another node -> web leader can start
+ appState.setRemoteOperational(ProcessId.ELASTICSEARCH);
+ processLauncher.waitForProcessAlive(WEB_SERVER);
+ assertThat(processLauncher.processes).hasSize(1);
+
+ underTest.terminate();
+ }
+
+ @Test
+ public void compute_engine_waits_for_remote_elasticsearch_and_web_leader_to_be_started_if_local_es_is_disabled() throws Exception {
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_WEB_DISABLED, "true");
+ SchedulerImpl underTest = newScheduler();
+ underTest.schedule();
+
+ // CE waits for ES and WEB leader to be up
+ assertThat(processLauncher.processes).isEmpty();
+
+ // ES and WEB leader become operational on another nodes -> CE can start
+ appState.setRemoteOperational(ProcessId.ELASTICSEARCH);
+ appState.setRemoteOperational(ProcessId.WEB_SERVER);
+
+ processLauncher.waitForProcessAlive(COMPUTE_ENGINE);
+ assertThat(processLauncher.processes).hasSize(1);
+
+ underTest.terminate();
+ }
+
+ private SchedulerImpl newScheduler() {
+ return new SchedulerImpl(settings, appReloader, javaCommandFactory, processLauncher, appState)
+ .setProcessWatcherDelayMs(1L);
+ }
+
+ private Scheduler startAll() throws InterruptedException {
+ enableAllProcesses();
+ SchedulerImpl scheduler = newScheduler();
+ scheduler.schedule();
+ processLauncher.waitForProcess(ELASTICSEARCH).operational = true;
+ processLauncher.waitForProcess(WEB_SERVER).operational = true;
+ processLauncher.waitForProcess(COMPUTE_ENGINE).operational = true;
+ return scheduler;
+ }
+
+ private void waitForAppStateOperational(ProcessId id) throws InterruptedException {
+ while (true) {
+ if (appState.isOperational(id, true)) {
+ return;
+ }
+ Thread.sleep(1L);
+ }
+ }
+
+ private class TestCommandFactory implements CommandFactory {
+ @Override
+ public EsCommand createEsCommand() {
+ return esCommand;
+ }
+
+ @Override
+ public JavaCommand createWebCommand(boolean leader) {
+ return leader ? webLeaderCommand : webFollowerCommand;
+ }
+
+ @Override
+ public JavaCommand createCeCommand() {
+ return ceCommand;
+ }
+ }
+
+ private class TestProcessLauncher implements ProcessLauncher {
+ private final EnumMap<ProcessId, TestProcess> processes = new EnumMap<>(ProcessId.class);
+ private final List<AbstractCommand<?>> commands = synchronizedList(new ArrayList<>());
+ private ProcessId makeStartupFail = null;
+
+ @Override
+ public ProcessMonitor launch(EsCommand esCommand) {
+ return launchImpl(esCommand);
+ }
+
+ @Override
+ public ProcessMonitor launch(JavaCommand javaCommand) {
+ return launchImpl(javaCommand);
+ }
+
+ private ProcessMonitor launchImpl(AbstractCommand<?> javaCommand) {
+ commands.add(javaCommand);
+ if (makeStartupFail == javaCommand.getProcessId()) {
+ throw new IllegalStateException("cannot start " + javaCommand.getProcessId());
+ }
+ TestProcess process = new TestProcess(javaCommand.getProcessId());
+ processes.put(javaCommand.getProcessId(), process);
+ return process;
+ }
+
+ private TestProcess waitForProcess(ProcessId id) throws InterruptedException {
+ while (true) {
+ TestProcess p = processes.get(id);
+ if (p != null) {
+ return p;
+ }
+ Thread.sleep(1L);
+ }
+ }
+
+ private TestProcess waitForProcessAlive(ProcessId id) throws InterruptedException {
+ while (true) {
+ TestProcess p = processes.get(id);
+ if (p != null && p.isAlive()) {
+ return p;
+ }
+ Thread.sleep(1L);
+ }
+ }
+
+ private TestProcess waitForProcessDown(ProcessId id) throws InterruptedException {
+ while (true) {
+ TestProcess p = processes.get(id);
+ if (p != null && !p.isAlive()) {
+ return p;
+ }
+ Thread.sleep(1L);
+ }
+ }
+
+ @Override
+ public void close() {
+ for (TestProcess process : processes.values()) {
+ process.destroyForcibly();
+ }
+ }
+ }
+
+ private class TestProcess implements ProcessMonitor, AutoCloseable {
+ private final ProcessId processId;
+ private final CountDownLatch alive = new CountDownLatch(1);
+ private boolean operational = false;
+ private boolean askedForRestart = false;
+
+ private TestProcess(ProcessId processId) {
+ this.processId = processId;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return mock(InputStream.class, Mockito.RETURNS_MOCKS);
+ }
+
+ @Override
+ public InputStream getErrorStream() {
+ return mock(InputStream.class, Mockito.RETURNS_MOCKS);
+ }
+
+ @Override
+ public void closeStreams() {
+ }
+
+ @Override
+ public boolean isAlive() {
+ return alive.getCount() == 1;
+ }
+
+ @Override
+ public void askForStop() {
+ destroyForcibly();
+ }
+
+ @Override
+ public void destroyForcibly() {
+ if (isAlive()) {
+ orderedStops.add(processId);
+ }
+ alive.countDown();
+ }
+
+ @Override
+ public void waitFor() throws InterruptedException {
+ alive.await();
+ }
+
+ @Override
+ public void waitFor(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
+ alive.await(timeout, timeoutUnit);
+ }
+
+ @Override
+ public boolean isOperational() {
+ return operational;
+ }
+
+ @Override
+ public boolean askedForRestart() {
+ return askedForRestart;
+ }
+
+ @Override
+ public void acknowledgeAskForRestart() {
+ this.askedForRestart = false;
+ }
+
+ @Override
+ public void close() {
+ alive.countDown();
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application;
+
+import java.util.ArrayList;
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicBoolean;
+import javax.annotation.Nonnull;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessId;
+
+public class TestAppState implements AppState {
+
+ private final Map<ProcessId, Boolean> localProcesses = new EnumMap<>(ProcessId.class);
+ private final Map<ProcessId, Boolean> remoteProcesses = new EnumMap<>(ProcessId.class);
+ private final List<AppStateListener> listeners = new ArrayList<>();
+ private final AtomicBoolean webLeaderLocked = new AtomicBoolean(false);
+
+ @Override
+ public void addListener(@Nonnull AppStateListener listener) {
+ this.listeners.add(listener);
+ }
+
+ @Override
+ public boolean isOperational(ProcessId processId, boolean local) {
+ if (local) {
+ return localProcesses.computeIfAbsent(processId, p -> false);
+ }
+ return remoteProcesses.computeIfAbsent(processId, p -> false);
+ }
+
+ @Override
+ public void setOperational(ProcessId processId) {
+ localProcesses.put(processId, true);
+ remoteProcesses.put(processId, true);
+ listeners.forEach(l -> l.onAppStateOperational(processId));
+ }
+
+ public void setRemoteOperational(ProcessId processId) {
+ remoteProcesses.put(processId, true);
+ listeners.forEach(l -> l.onAppStateOperational(processId));
+ }
+
+ @Override
+ public boolean tryToLockWebLeader() {
+ return webLeaderLocked.compareAndSet(false, true);
+ }
+
+ @Override
+ public void reset() {
+ webLeaderLocked.set(false);
+ localProcesses.clear();
+ }
+
+ @Override
+ public void registerSonarQubeVersion(String sonarqubeVersion) {
+ // nothing to do
+ }
+
+ @Override
+ public Optional<String> getLeaderHostName() {
+ return Optional.of(NetworkUtils.getHostName());
+ }
+
+ @Override
+ public void close() {
+ // nothing to do
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.cluster;
+
+import com.hazelcast.core.HazelcastInstance;
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.slf4j.Logger;
+import org.sonar.application.AppStateListener;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.sonar.application.cluster.HazelcastTestHelper.createHazelcastClient;
+import static org.sonar.application.cluster.HazelcastTestHelper.newClusterSettings;
+import static org.sonar.process.cluster.ClusterObjectKeys.SONARQUBE_VERSION;
+
+public class AppStateClusterImplTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Rule
+ public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
+
+ @Test
+ public void instantiation_throws_ISE_if_cluster_mode_is_disabled() throws Exception {
+ TestAppSettings settings = new TestAppSettings();
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "false");
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Cluster is not enabled on this instance");
+
+ new AppStateClusterImpl(settings);
+ }
+
+ @Test
+ public void tryToLockWebLeader_returns_true_only_for_the_first_call() throws Exception {
+ TestAppSettings settings = newClusterSettings();
+
+ try (AppStateClusterImpl underTest = new AppStateClusterImpl(settings)) {
+ assertThat(underTest.tryToLockWebLeader()).isEqualTo(true);
+ assertThat(underTest.tryToLockWebLeader()).isEqualTo(false);
+ }
+ }
+
+ @Test
+ public void log_when_sonarqube_is_joining_a_cluster () throws IOException, InterruptedException, IllegalAccessException, NoSuchFieldException {
+ // Now launch an instance that try to be part of the hzInstance cluster
+ TestAppSettings settings = newClusterSettings();
+
+ Logger logger = mock(Logger.class);
+ AppStateClusterImpl.setLogger(logger);
+
+ try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
+ verify(logger).info(
+ eq("Joined the cluster [{}] that contains the following hosts : [{}]"),
+ eq("sonarqube"),
+ anyString()
+ );
+ }
+ }
+
+ @Test
+ public void test_listeners() throws InterruptedException {
+ AppStateListener listener = mock(AppStateListener.class);
+ try (AppStateClusterImpl underTest = new AppStateClusterImpl(newClusterSettings())) {
+ underTest.addListener(listener);
+
+ underTest.setOperational(ProcessId.ELASTICSEARCH);
+ verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
+
+ assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isEqualTo(true);
+ assertThat(underTest.isOperational(ProcessId.APP, true)).isEqualTo(false);
+ assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isEqualTo(false);
+ assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isEqualTo(false);
+ }
+ }
+
+ @Test
+ public void registerSonarQubeVersion_publishes_version_on_first_call() {
+ TestAppSettings settings = newClusterSettings();
+
+ try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
+ appStateCluster.registerSonarQubeVersion("6.4.1.5");
+
+ HazelcastInstance hzInstance = createHazelcastClient(appStateCluster);
+ assertThat(hzInstance.getAtomicReference(SONARQUBE_VERSION).get())
+ .isNotNull()
+ .isInstanceOf(String.class)
+ .isEqualTo("6.4.1.5");
+ }
+ }
+
+ @Test
+ public void reset_throws_always_ISE() {
+ TestAppSettings settings = newClusterSettings();
+
+ try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("state reset is not supported in cluster mode");
+ appStateCluster.reset();
+ }
+ }
+
+ @Test
+ public void registerSonarQubeVersion_throws_ISE_if_initial_version_is_different() throws Exception {
+ // Now launch an instance that try to be part of the hzInstance cluster
+ TestAppSettings settings = newClusterSettings();
+
+ try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
+ // Register first version
+ appStateCluster.registerSonarQubeVersion("1.0.0");
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("The local version 2.0.0 is not the same as the cluster 1.0.0");
+
+ // Registering a second different version must trigger an exception
+ appStateCluster.registerSonarQubeVersion("2.0.0");
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.cluster;
+
+import org.junit.Test;
+import org.sonar.process.ProcessId;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ClusterProcessTest {
+ @Test
+ public void test_equality() {
+ ClusterProcess clusterProcess = new ClusterProcess("A", ProcessId.WEB_SERVER);
+
+ assertThat(clusterProcess)
+ .isNotEqualTo(null)
+ .isEqualTo(clusterProcess)
+ .isNotEqualTo(new ClusterProcess("B", ProcessId.WEB_SERVER))
+ .isNotEqualTo(new ClusterProcess("A", ProcessId.ELASTICSEARCH))
+ .isEqualTo(new ClusterProcess("A", ProcessId.WEB_SERVER));
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.cluster;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.application.config.AppSettings;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.ProcessProperties;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.stream.Stream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class ClusterPropertiesTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private AppSettings appSettings = new TestAppSettings();
+
+ @Test
+ public void test_default_values() throws Exception {
+ ClusterProperties props = new ClusterProperties(appSettings);
+
+ assertThat(props.getNetworkInterfaces())
+ .isEqualTo(Collections.emptyList());
+ assertThat(props.getPort())
+ .isEqualTo(9003);
+ assertThat(props.isEnabled())
+ .isEqualTo(false);
+ assertThat(props.getHosts())
+ .isEqualTo(Collections.emptyList());
+ assertThat(props.getName())
+ .isEqualTo("sonarqube");
+ }
+
+ @Test
+ public void test_port_parameter() {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+
+ Stream.of("-50", "0", "65536", "128563").forEach(
+ port -> {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_PORT, port);
+
+ ClusterProperties clusterProperties = new ClusterProperties(appSettings);
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage(
+ String.format("Cluster port have been set to %s which is outside the range [1-65535].", port));
+ clusterProperties.validate();
+
+ });
+ }
+
+ @Test
+ public void test_interfaces_parameter() {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_NETWORK_INTERFACES, "8.8.8.8"); // This IP belongs to Google
+
+ ClusterProperties clusterProperties = new ClusterProperties(appSettings);
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage(
+ String.format("Interface %s is not available on this machine.", "8.8.8.8"));
+ clusterProperties.validate();
+ }
+
+ @Test
+ public void validate_does_not_fail_if_cluster_enabled_and_name_specified() {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+
+ ClusterProperties clusterProperties = new ClusterProperties(appSettings);
+ clusterProperties.validate();
+ }
+
+ @Test
+ public void test_members() {
+ appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
+ appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+
+ assertThat(
+ new ClusterProperties(appSettings).getHosts()).isEqualTo(
+ Collections.emptyList());
+
+ appSettings.getProps().set(ProcessProperties.CLUSTER_HOSTS, "192.168.1.1");
+ assertThat(
+ new ClusterProperties(appSettings).getHosts()).isEqualTo(
+ Arrays.asList("192.168.1.1:9003"));
+
+ appSettings.getProps().set(ProcessProperties.CLUSTER_HOSTS, "192.168.1.2:5501");
+ assertThat(
+ new ClusterProperties(appSettings).getHosts()).containsExactlyInAnyOrder(
+ "192.168.1.2:5501");
+
+ appSettings.getProps().set(ProcessProperties.CLUSTER_HOSTS, "192.168.1.2:5501,192.168.1.1");
+ assertThat(
+ new ClusterProperties(appSettings).getHosts()).containsExactlyInAnyOrder(
+ "192.168.1.2:5501", "192.168.1.1:9003");
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.cluster;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+import com.hazelcast.core.HazelcastInstance;
+import com.hazelcast.core.ItemEvent;
+import com.hazelcast.core.ItemListener;
+import com.hazelcast.core.ReplicatedMap;
+import java.net.InetAddress;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.AfterClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.slf4j.LoggerFactory;
+import org.sonar.application.AppStateListener;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.cluster.ClusterObjectKeys;
+
+import static junit.framework.TestCase.fail;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.sonar.application.cluster.HazelcastTestHelper.closeAllHazelcastClients;
+import static org.sonar.application.cluster.HazelcastTestHelper.createHazelcastClient;
+import static org.sonar.application.cluster.HazelcastTestHelper.newClusterSettings;
+import static org.sonar.process.ProcessProperties.CLUSTER_NAME;
+import static org.sonar.process.cluster.ClusterObjectKeys.LEADER;
+import static org.sonar.process.cluster.ClusterObjectKeys.OPERATIONAL_PROCESSES;
+import static org.sonar.process.cluster.ClusterObjectKeys.SONARQUBE_VERSION;
+
+public class HazelcastClusterTest {
+ @Rule
+ public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @AfterClass
+ public static void closeHazelcastClients() {
+ closeAllHazelcastClients();
+ }
+
+ @Test
+ public void test_two_tryToLockWebLeader_must_return_true() {
+ ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ assertThat(hzCluster.tryToLockWebLeader()).isEqualTo(true);
+ assertThat(hzCluster.tryToLockWebLeader()).isEqualTo(false);
+ }
+ }
+
+ @Test
+ public void when_another_process_locked_webleader_tryToLockWebLeader_must_return_false() {
+ ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
+ hzInstance.getAtomicReference(LEADER).set("aaaa");
+ assertThat(hzCluster.tryToLockWebLeader()).isEqualTo(false);
+ }
+ }
+
+ @Test
+ public void when_no_leader_getLeaderHostName_must_return_NO_LEADER() {
+ ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ assertThat(hzCluster.getLeaderHostName()).isEmpty();
+ }
+ }
+
+ @Test
+ public void when_no_leader_getLeaderHostName_must_return_the_hostname() {
+ ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ assertThat(hzCluster.tryToLockWebLeader()).isTrue();
+ assertThat(hzCluster.getLeaderHostName().get()).isEqualTo(NetworkUtils.getHostName());
+ }
+ }
+
+ @Test
+ public void members_must_be_empty_when_there_is_no_other_node() {
+ ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ assertThat(hzCluster.getMembers()).isEmpty();
+ }
+ }
+
+ @Test
+ public void set_operational_is_writing_to_cluster() {
+ ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+
+ hzCluster.setOperational(ProcessId.ELASTICSEARCH);
+
+ assertThat(hzCluster.isOperational(ProcessId.ELASTICSEARCH)).isTrue();
+ assertThat(hzCluster.isOperational(ProcessId.WEB_SERVER)).isFalse();
+ assertThat(hzCluster.isOperational(ProcessId.COMPUTE_ENGINE)).isFalse();
+
+ // Connect via Hazelcast client to test values
+ HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
+ ReplicatedMap<ClusterProcess, Boolean> operationalProcesses = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
+ assertThat(operationalProcesses)
+ .containsExactly(new AbstractMap.SimpleEntry<>(new ClusterProcess(hzCluster.getLocalUUID(), ProcessId.ELASTICSEARCH), Boolean.TRUE));
+ }
+ }
+
+ @Test
+ public void cluster_name_comes_from_configuration() {
+ TestAppSettings testAppSettings = newClusterSettings();
+ testAppSettings.set(CLUSTER_NAME, "a_cluster_");
+ ClusterProperties clusterProperties = new ClusterProperties(testAppSettings);
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ assertThat(hzCluster.getName()).isEqualTo("a_cluster_");
+ }
+ }
+
+ @Test
+ public void cluster_must_keep_a_list_of_clients() throws InterruptedException {
+ TestAppSettings testAppSettings = newClusterSettings();
+ testAppSettings.set(CLUSTER_NAME, "a_cluster_");
+ ClusterProperties clusterProperties = new ClusterProperties(testAppSettings);
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ assertThat(hzCluster.hzInstance.getSet(ClusterObjectKeys.CLIENT_UUIDS)).isEmpty();
+ HazelcastInstance hzClient = HazelcastTestHelper.createHazelcastClient(hzCluster);
+ assertThat(hzCluster.hzInstance.getSet(ClusterObjectKeys.CLIENT_UUIDS)).containsExactly(hzClient.getLocalEndpoint().getUuid());
+
+ CountDownLatch latch = new CountDownLatch(1);
+ hzCluster.hzInstance.getSet(ClusterObjectKeys.CLIENT_UUIDS).addItemListener(new ItemListener<Object>() {
+ @Override
+ public void itemAdded(ItemEvent<Object> item) {
+ }
+
+ @Override
+ public void itemRemoved(ItemEvent<Object> item) {
+ latch.countDown();
+ }
+ }, false);
+
+ hzClient.shutdown();
+ if (latch.await(5, TimeUnit.SECONDS)) {
+ assertThat(hzCluster.hzInstance.getSet(ClusterObjectKeys.CLIENT_UUIDS).size()).isEqualTo(0);
+ } else {
+ fail("The client UUID have not been removed from the Set within 5 seconds' time lapse");
+ }
+ }
+ }
+
+ @Test
+ public void localUUID_must_not_be_empty() {
+ ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ assertThat(hzCluster.getLocalUUID()).isNotEmpty();
+ }
+ }
+
+ @Test
+ public void when_a_process_is_set_operational_listener_must_be_triggered() {
+ ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ AppStateListener listener = mock(AppStateListener.class);
+ hzCluster.addListener(listener);
+
+ // ElasticSearch is not operational
+ assertThat(hzCluster.isOperational(ProcessId.ELASTICSEARCH)).isFalse();
+
+ // Simulate a node that set ElasticSearch operational
+ HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
+ ReplicatedMap<ClusterProcess, Boolean> operationalProcesses = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
+ operationalProcesses.put(new ClusterProcess(UUID.randomUUID().toString(), ProcessId.ELASTICSEARCH), Boolean.TRUE);
+ verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
+ verifyNoMoreInteractions(listener);
+
+ // ElasticSearch is operational
+ assertThat(hzCluster.isOperational(ProcessId.ELASTICSEARCH)).isTrue();
+ }
+ }
+
+
+ @Test
+ public void registerSonarQubeVersion_publishes_version_on_first_call() {
+ ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ hzCluster.registerSonarQubeVersion("1.0.0.0");
+
+ HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
+ assertThat(hzInstance.getAtomicReference(SONARQUBE_VERSION).get()).isEqualTo("1.0.0.0");
+ }
+ }
+
+ @Test
+ public void registerSonarQubeVersion_throws_ISE_if_initial_version_is_different() throws Exception {
+ ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
+ // Register first version
+ hzCluster.registerSonarQubeVersion("1.0.0");
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("The local version 2.0.0 is not the same as the cluster 1.0.0");
+
+ // Registering a second different version must trigger an exception
+ hzCluster.registerSonarQubeVersion("2.0.0");
+ }
+ }
+
+ @Test
+ public void simulate_network_cluster() throws InterruptedException {
+ TestAppSettings settings = newClusterSettings();
+ settings.set(ProcessProperties.CLUSTER_NETWORK_INTERFACES, InetAddress.getLoopbackAddress().getHostAddress());
+ AppStateListener listener = mock(AppStateListener.class);
+
+ try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
+ appStateCluster.addListener(listener);
+
+ HazelcastInstance hzInstance = createHazelcastClient(appStateCluster.getHazelcastCluster());
+ String uuid = UUID.randomUUID().toString();
+ ReplicatedMap<ClusterProcess, Boolean> replicatedMap = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
+ // process is not up yet --> no events are sent to listeners
+ replicatedMap.put(
+ new ClusterProcess(uuid, ProcessId.ELASTICSEARCH),
+ Boolean.FALSE);
+
+ // process is up yet --> notify listeners
+ replicatedMap.replace(
+ new ClusterProcess(uuid, ProcessId.ELASTICSEARCH),
+ Boolean.TRUE);
+
+ // should be called only once
+ verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
+ verifyNoMoreInteractions(listener);
+
+ hzInstance.shutdown();
+ }
+ }
+
+ @Test
+ public void hazelcast_must_log_through_sl4fj() {
+ MemoryAppender<ILoggingEvent> memoryAppender = new MemoryAppender<>();
+ LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
+ lc.reset();
+ memoryAppender.setContext(lc);
+ memoryAppender.start();
+ lc.getLogger("com.hazelcast").addAppender(memoryAppender);
+
+ try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(newClusterSettings())) {
+ }
+
+ assertThat(memoryAppender.events).isNotEmpty();
+ memoryAppender.events.stream().forEach(
+ e -> assertThat(e.getLoggerName()).startsWith("com.hazelcast")
+ );
+ }
+
+ private class MemoryAppender<E> extends AppenderBase<E> {
+ private final List<E> events = new ArrayList();
+
+ @Override
+ protected void append(E eventObject) {
+ events.add(eventObject);
+ }
+ }
+
+
+ @Test
+ public void configuration_tweaks_of_hazelcast_must_be_present() {
+ try (HazelcastCluster hzCluster = HazelcastCluster.create(new ClusterProperties(newClusterSettings()))) {
+ assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.tcp.join.port.try.count")).isEqualTo("10");
+ assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.phone.home.enabled")).isEqualTo("false");
+ assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.logging.type")).isEqualTo("slf4j");
+ assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.socket.bind.any")).isEqualTo("false");
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.cluster;
+
+import com.hazelcast.client.HazelcastClient;
+import com.hazelcast.client.config.ClientConfig;
+import com.hazelcast.core.HazelcastInstance;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.application.config.TestAppSettings;
+import org.sonar.process.ProcessProperties;
+
+public class HazelcastTestHelper {
+
+ // Be careful this test won't work if parallel tests is used
+ private static final List<HazelcastInstance> HAZELCAST_INSTANCES = new ArrayList<>();
+
+ static HazelcastInstance createHazelcastClient(HazelcastCluster hzCluster) {
+ ClientConfig clientConfig = new ClientConfig();
+ InetSocketAddress socketAddress = (InetSocketAddress) hzCluster.hzInstance.getLocalEndpoint().getSocketAddress();
+
+ clientConfig.getNetworkConfig().getAddresses().add(
+ String.format("%s:%d",
+ socketAddress.getHostString(),
+ socketAddress.getPort()
+ ));
+ clientConfig.getGroupConfig().setName(hzCluster.getName());
+ HazelcastInstance hazelcastInstance = HazelcastClient.newHazelcastClient(clientConfig);
+ HAZELCAST_INSTANCES.add(hazelcastInstance);
+ return hazelcastInstance;
+ }
+
+ static void closeAllHazelcastClients() {
+ HAZELCAST_INSTANCES.stream().forEach(
+ hz -> {
+ try {
+ hz.shutdown();
+ } catch (Exception ex) {
+ // Ignore it
+ }
+ }
+ );
+ }
+
+ static HazelcastInstance createHazelcastClient(AppStateClusterImpl appStateCluster) {
+ return createHazelcastClient(appStateCluster.getHazelcastCluster());
+ }
+
+ static TestAppSettings newClusterSettings() {
+ TestAppSettings settings = new TestAppSettings();
+ settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_NAME, "sonarqube");
+ return settings;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.util.Properties;
+import org.junit.Test;
+import org.sonar.process.Props;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class AppSettingsImplTest {
+
+ @Test
+ public void reload_updates_properties() {
+ Props initialProps = new Props(new Properties());
+ initialProps.set("foo", "bar");
+ Props newProps = new Props(new Properties());
+ newProps.set("foo", "baz");
+ newProps.set("newProp", "newVal");
+
+ AppSettingsImpl underTest = new AppSettingsImpl(initialProps);
+ underTest.reload(newProps);
+
+ assertThat(underTest.getValue("foo").get()).isEqualTo("baz");
+ assertThat(underTest.getValue("newProp").get()).isEqualTo("newVal");
+ assertThat(underTest.getProps().rawProperties()).hasSize(2);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.io.File;
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.data.MapEntry.entry;
+
+public class AppSettingsLoaderImplTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void load_properties_from_file() throws Exception {
+ File homeDir = temp.newFolder();
+ File propsFile = new File(homeDir, "conf/sonar.properties");
+ FileUtils.write(propsFile, "foo=bar");
+
+ AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir);
+ AppSettings settings = underTest.load();
+
+ assertThat(settings.getProps().rawProperties()).contains(entry("foo", "bar"));
+ }
+
+ @Test
+ public void throws_ISE_if_file_fails_to_be_loaded() throws Exception {
+ File homeDir = temp.newFolder();
+ File propsFileAsDir = new File(homeDir, "conf/sonar.properties");
+ FileUtils.forceMkdir(propsFileAsDir);
+ AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir);
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Cannot open file " + propsFileAsDir.getAbsolutePath());
+
+ underTest.load();
+ }
+
+ @Test
+ public void file_is_not_loaded_if_it_does_not_exist() throws Exception {
+ File homeDir = temp.newFolder();
+
+ AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir);
+ AppSettings settings = underTest.load();
+
+ // no failure, file is ignored
+ assertThat(settings.getProps()).isNotNull();
+ }
+
+ @Test
+ public void command_line_arguments_are_included_to_settings() throws Exception {
+ File homeDir = temp.newFolder();
+
+ AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[] {"-Dsonar.foo=bar", "-Dhello=world"}, homeDir);
+ AppSettings settings = underTest.load();
+
+ assertThat(settings.getProps().rawProperties())
+ .contains(entry("sonar.foo", "bar"))
+ .contains(entry("hello", "world"));
+ }
+
+ @Test
+ public void command_line_arguments_make_precedence_over_properties_files() throws Exception {
+ File homeDir = temp.newFolder();
+ File propsFile = new File(homeDir, "conf/sonar.properties");
+ FileUtils.write(propsFile, "sonar.foo=file");
+
+ AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[]{"-Dsonar.foo=cli"}, homeDir);
+ AppSettings settings = underTest.load();
+
+ assertThat(settings.getProps().rawProperties()).contains(entry("sonar.foo", "cli"));
+ }
+
+ @Test
+ public void detectHomeDir_returns_existing_dir() throws Exception {
+ assertThat(new AppSettingsLoaderImpl(new String[0]).getHomeDir()).exists().isDirectory();
+
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.config;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Collections;
+import java.util.Enumeration;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.experimental.theories.DataPoints;
+import org.junit.experimental.theories.FromDataPoints;
+import org.junit.experimental.theories.Theories;
+import org.junit.experimental.theories.Theory;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.process.MessageException;
+
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_NETWORK_INTERFACES;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.JDBC_URL;
+import static org.sonar.process.ProcessProperties.SEARCH_HOST;
+
+@RunWith(Theories.class)
+public class ClusterSettingsLoopbackTest {
+
+ private TestAppSettings settings;
+ private static final String LOOPBACK_FORBIDDEN = " must not be a loopback address";
+ private static final String NOT_LOCAL_ADDRESS = " is not a local address";
+ private static final String NOT_RESOLVABLE = " cannot be resolved";
+
+ @DataPoints("parameter")
+ public static final ValueAndResult[] VALID_SINGLE_IP = {
+ // Valid IPs
+ new ValueAndResult("1.2.3.4", NOT_LOCAL_ADDRESS),
+ new ValueAndResult("1.2.3.4:9001", NOT_LOCAL_ADDRESS),
+ new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb", NOT_LOCAL_ADDRESS),
+ new ValueAndResult("[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001", NOT_LOCAL_ADDRESS),
+
+ // Valid Name
+ new ValueAndResult("www.sonarqube.org", NOT_LOCAL_ADDRESS),
+ new ValueAndResult("www.google.fr", NOT_LOCAL_ADDRESS),
+ new ValueAndResult("www.google.com, www.sonarsource.com, wwww.sonarqube.org", NOT_LOCAL_ADDRESS),
+
+ new ValueAndResult("...", NOT_RESOLVABLE),
+ new ValueAndResult("භඦආ\uD801\uDC8C\uD801\uDC8B", NOT_RESOLVABLE),
+
+ // Valide IPs List
+ new ValueAndResult("1.2.3.4,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb", NOT_LOCAL_ADDRESS),
+ new ValueAndResult("1.2.3.4:9001,[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001", NOT_LOCAL_ADDRESS),
+ new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,1.2.3.4:9001", NOT_LOCAL_ADDRESS),
+ new ValueAndResult("[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccc", NOT_LOCAL_ADDRESS),
+
+ // Loopback IPs
+ new ValueAndResult("localhost", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("127.0.0.1", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("127.1.1.1", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("127.243.136.241", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("::1", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("0:0:0:0:0:0:0:1", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("localhost:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("127.0.0.1:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("127.1.1.1:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("127.243.136.241:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("[::1]:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("[0:0:0:0:0:0:0:1]:9001", LOOPBACK_FORBIDDEN),
+
+ // Loopback IPs list
+ new ValueAndResult("127.0.0.1,192.168.11.25", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("192.168.11.25,127.1.1.1", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,0:0:0:0:0:0:0:1", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("0:0:0:0:0:0:0:1,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb,::1", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("::1,2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("::1,2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb,2a01:e34:ef1f:dbb0:b3f6:a978:c5c0:9ccb", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("localhost:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("127.0.0.1:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("127.1.1.1:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("127.243.136.241:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("[::1]:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("[0:0:0:0:0:0:0:1]:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("127.0.0.1,192.168.11.25:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("192.168.11.25:9001,127.1.1.1:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,[0:0:0:0:0:0:0:1]:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("[0:0:0:0:0:0:0:1]:9001,[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("[2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb]:9001,[::1]:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("[::1]:9001,[2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb]:9001", LOOPBACK_FORBIDDEN),
+ new ValueAndResult("[::1]:9001,[2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb]:9001,[2a01:e34:ef1f:dbb0:b3f6:a978:c5c0:9ccb]:9001", LOOPBACK_FORBIDDEN)
+ };
+
+ @DataPoints("key")
+ public static final Key[] KEYS = {
+ new Key(SEARCH_HOST, false, false),
+ new Key(CLUSTER_NETWORK_INTERFACES, true, false),
+ new Key(CLUSTER_SEARCH_HOSTS, true, true),
+ new Key(CLUSTER_HOSTS, true, true)
+ };
+
+
+ @DataPoints("unresolvable_hosts")
+ public static final String[] UNRESOLVABLE_HOSTS = {
+ };
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Before
+ public void resetSettings() {
+ settings = getClusterSettings();
+ }
+
+ @Theory
+ public void accept_throws_MessageException(
+ @FromDataPoints("key") Key propertyKey,
+ @FromDataPoints("parameter") ValueAndResult valueAndResult) {
+ // Skip the test if the value is a list and if the key is not accepting a list
+ if (settings == null) {
+ System.out.println("No network found, skipping the test");
+ return;
+ }
+ if ((valueAndResult.isList() && propertyKey.acceptList) || !valueAndResult.isList()) {
+ settings.set(propertyKey.getKey(), valueAndResult.getValue());
+
+ // If the key accept non local IPs there won't be any exception
+ if (!propertyKey.acceptNonLocal || valueAndResult.getMessage() != NOT_LOCAL_ADDRESS) {
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage(valueAndResult.getMessage());
+ }
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+ }
+
+ private static TestAppSettings getClusterSettings() {
+ String localAddress = null;
+ try {
+ Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
+ for (NetworkInterface networkInterface : Collections.list(nets)) {
+ if (!networkInterface.isLoopback() && networkInterface.isUp()) {
+ localAddress = networkInterface.getInetAddresses().nextElement().getHostAddress();
+ }
+ }
+ if (localAddress == null) {
+ return null;
+ }
+
+ } catch (SocketException e) {
+ return null;
+ }
+
+ TestAppSettings testAppSettings = new TestAppSettings()
+ .set(CLUSTER_ENABLED, "true")
+ .set(CLUSTER_SEARCH_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
+ .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
+ .set(SEARCH_HOST, localAddress)
+ .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
+ return testAppSettings;
+ }
+
+ private static class Key {
+ private final String key;
+ private final boolean acceptList;
+ private final boolean acceptNonLocal;
+
+ private Key(String key, boolean acceptList, boolean acceptNonLocal) {
+ this.key = key;
+ this.acceptList = acceptList;
+ this.acceptNonLocal = acceptNonLocal;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public boolean acceptList() {
+ return acceptList;
+ }
+
+ public boolean acceptNonLocal() {
+ return acceptNonLocal;
+ }
+ }
+
+ private static class ValueAndResult {
+ private final String value;
+ private final String message;
+
+ private ValueAndResult(String value, String message) {
+ this.value = value;
+ this.message = message;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public boolean isList() {
+ return value != null && value.contains(",");
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.process.MessageException;
+import org.sonar.process.ProcessProperties;
+
+import static java.lang.String.format;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
+import static org.sonar.process.ProcessId.ELASTICSEARCH;
+import static org.sonar.process.ProcessId.WEB_SERVER;
+import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_DISABLED;
+import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
+import static org.sonar.process.ProcessProperties.JDBC_URL;
+import static org.sonar.process.ProcessProperties.SEARCH_HOST;
+
+
+public class ClusterSettingsTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private TestAppSettings settings;
+
+ @Before
+ public void resetSettings() {
+ settings = getClusterSettings();
+ }
+
+ @Test
+ public void test_isClusterEnabled() {
+ settings.set(CLUSTER_ENABLED, "true");
+ assertThat(ClusterSettings.isClusterEnabled(settings)).isTrue();
+
+ settings.set(CLUSTER_ENABLED, "false");
+ assertThat(ClusterSettings.isClusterEnabled(settings)).isFalse();
+ }
+
+ @Test
+ public void isClusterEnabled_returns_false_by_default() {
+ assertThat(ClusterSettings.isClusterEnabled(new TestAppSettings())).isFalse();
+ }
+
+ @Test
+ public void getEnabledProcesses_returns_all_processes_by_default() {
+ assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, ELASTICSEARCH, WEB_SERVER);
+ }
+
+ @Test
+ public void getEnabledProcesses_returns_all_processes_by_default_in_cluster_mode() {
+ settings.set(CLUSTER_ENABLED, "true");
+
+ assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, ELASTICSEARCH, WEB_SERVER);
+ }
+
+ @Test
+ public void getEnabledProcesses_returns_configured_processes_in_cluster_mode() {
+ settings.set(CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
+
+ assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, WEB_SERVER);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_internal_property_for_web_leader_is_configured() {
+ settings.set(CLUSTER_ENABLED, "true");
+ settings.set("sonar.cluster.web.startupLeader", "true");
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Property [sonar.cluster.web.startupLeader] is forbidden");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_search_enabled_with_loopback() {
+ settings.set(CLUSTER_ENABLED, "true");
+ settings.set(CLUSTER_SEARCH_DISABLED, "false");
+ settings.set(CLUSTER_SEARCH_HOSTS, "192.168.1.1,192.168.1.2");
+ settings.set(SEARCH_HOST, "::1");
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("The interface address [::1] of [sonar.search.host] must not be a loopback address");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Test
+ public void accept_not_throwing_MessageException_if_search_disabled_with_loopback() {
+ settings.set(CLUSTER_ENABLED, "true");
+ settings.set(CLUSTER_SEARCH_DISABLED, "true");
+ settings.set(CLUSTER_SEARCH_HOSTS, "192.168.1.1,192.168.1.2");
+ settings.set(SEARCH_HOST, "127.0.0.1");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Test
+ public void accept_does_nothing_if_cluster_is_disabled() {
+ settings.set(CLUSTER_ENABLED, "false");
+ // this property is supposed to fail if cluster is enabled
+ settings.set("sonar.cluster.web.startupLeader", "true");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_h2() {
+ settings.set(CLUSTER_ENABLED, "true");
+ settings.set("sonar.jdbc.url", "jdbc:h2:mem");
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Embedded database is not supported in cluster mode");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_default_jdbc_url() {
+ settings.clearProperty(JDBC_URL);
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Embedded database is not supported in cluster mode");
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ @Test
+ public void isLocalElasticsearchEnabled_returns_true_by_default() {
+ assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isTrue();
+ }
+
+ @Test
+ public void isLocalElasticsearchEnabled_returns_true_by_default_in_cluster_mode() {
+ assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isTrue();
+ }
+
+ @Test
+ public void isLocalElasticsearchEnabled_returns_false_if_local_es_node_is_disabled_in_cluster_mode() {
+ settings.set(CLUSTER_ENABLED, "true");
+ settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
+
+ assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isFalse();
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_searchHost_is_missing() {
+ settings.clearProperty(SEARCH_HOST);
+ checkMandatoryProperty(SEARCH_HOST);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_searchHost_is_blank() {
+ settings.set(SEARCH_HOST, " ");
+ checkMandatoryProperty(SEARCH_HOST);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_clusterHosts_is_missing() {
+ settings.clearProperty(CLUSTER_HOSTS);
+ checkMandatoryProperty(CLUSTER_HOSTS);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_clusterHosts_is_blank() {
+ settings.set(CLUSTER_HOSTS, " ");
+ checkMandatoryProperty(CLUSTER_HOSTS);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_clusterSearchHosts_is_missing() {
+ settings.clearProperty(CLUSTER_SEARCH_HOSTS);
+ checkMandatoryProperty(CLUSTER_SEARCH_HOSTS);
+ }
+
+ @Test
+ public void accept_throws_MessageException_if_clusterSearchHosts_is_blank() {
+ settings.set(CLUSTER_SEARCH_HOSTS, " ");
+ checkMandatoryProperty(CLUSTER_SEARCH_HOSTS);
+ }
+
+ private void checkMandatoryProperty(String key) {
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage(format("Property [%s] is mandatory", key));
+
+ new ClusterSettings().accept(settings.getProps());
+ }
+
+ private static TestAppSettings getClusterSettings() {
+ TestAppSettings testAppSettings = new TestAppSettings()
+ .set(CLUSTER_ENABLED, "true")
+ .set(CLUSTER_SEARCH_HOSTS, "localhost")
+ .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
+ .set(SEARCH_HOST, "192.168.233.1")
+ .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
+ return testAppSettings;
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.util.Properties;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CommandLineParserTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void parseArguments() {
+ System.setProperty("CommandLineParserTest.unused", "unused");
+ System.setProperty("sonar.CommandLineParserTest.used", "used");
+
+ Properties p = CommandLineParser.parseArguments(new String[] {"-Dsonar.foo=bar"});
+
+ // test environment can already declare some system properties prefixed by "sonar."
+ // so we can't test the exact number "2"
+ assertThat(p.size()).isGreaterThanOrEqualTo(2);
+ assertThat(p.getProperty("sonar.foo")).isEqualTo("bar");
+ assertThat(p.getProperty("sonar.CommandLineParserTest.used")).isEqualTo("used");
+
+ }
+
+ @Test
+ public void argumentsToProperties_throws_IAE_if_argument_does_not_start_with_minusD() {
+ Properties p = CommandLineParser.argumentsToProperties(new String[] {"-Dsonar.foo=bar", "-Dsonar.whitespace=foo bar"});
+ assertThat(p).hasSize(2);
+ assertThat(p.getProperty("sonar.foo")).isEqualTo("bar");
+ assertThat(p.getProperty("sonar.whitespace")).isEqualTo("foo bar");
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Command-line argument must start with -D, for example -Dsonar.jdbc.username=sonar. Got: sonar.bad=true");
+
+ CommandLineParser.argumentsToProperties(new String[] {"-Dsonar.foo=bar", "sonar.bad=true"});
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.io.File;
+import java.util.Properties;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.process.Props;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.process.ProcessProperties.PATH_DATA;
+import static org.sonar.process.ProcessProperties.PATH_HOME;
+import static org.sonar.process.ProcessProperties.PATH_LOGS;
+import static org.sonar.process.ProcessProperties.PATH_TEMP;
+import static org.sonar.process.ProcessProperties.PATH_WEB;
+
+
+public class FileSystemSettingsTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private FileSystemSettings underTest = new FileSystemSettings();
+ private File homeDir;
+
+ @Before
+ public void setUp() throws Exception {
+ homeDir = temp.newFolder();
+ }
+
+ @Test
+ public void relative_paths_are_converted_to_absolute_paths() throws Exception {
+ Props props = new Props(new Properties());
+ props.set(PATH_HOME, homeDir.getAbsolutePath());
+
+ // relative paths
+ props.set(PATH_DATA, "data");
+ props.set(PATH_LOGS, "logs");
+ props.set(PATH_TEMP, "temp");
+
+ // already absolute paths
+ props.set(PATH_WEB, new File(homeDir, "web").getAbsolutePath());
+
+ underTest.accept(props);
+
+ assertThat(props.nonNullValue(PATH_DATA)).isEqualTo(new File(homeDir, "data").getAbsolutePath());
+ assertThat(props.nonNullValue(PATH_LOGS)).isEqualTo(new File(homeDir, "logs").getAbsolutePath());
+ assertThat(props.nonNullValue(PATH_TEMP)).isEqualTo(new File(homeDir, "temp").getAbsolutePath());
+ assertThat(props.nonNullValue(PATH_WEB)).isEqualTo(new File(homeDir, "web").getAbsolutePath());
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.config;
+
+import java.io.File;
+import java.net.InetAddress;
+import java.util.Properties;
+import org.apache.commons.io.FileUtils;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.process.MessageException;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.application.config.JdbcSettings.Provider;
+import static org.sonar.process.ProcessProperties.JDBC_URL;
+
+public class JdbcSettingsTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private JdbcSettings underTest = new JdbcSettings();
+ private File homeDir;
+
+ @Before
+ public void setUp() throws Exception {
+ homeDir = temp.newFolder();
+ }
+
+ @Test
+ public void resolve_H2_provider_when_props_is_empty_and_set_URL_to_default_H2() {
+ Props props = newProps();
+
+ assertThat(underTest.resolveProviderAndEnforceNonnullJdbcUrl(props))
+ .isEqualTo(Provider.H2);
+ assertThat(props.nonNullValue(JDBC_URL)).isEqualTo(String.format("jdbc:h2:tcp://%s:9092/sonar", InetAddress.getLoopbackAddress().getHostAddress()));
+ }
+
+ @Test
+ public void resolve_Oracle_when_jdbc_url_contains_oracle_in_any_case() {
+ checkProviderForUrlAndUnchangedUrl("jdbc:oracle:foo", Provider.ORACLE);
+ checkProviderForUrlAndUnchangedUrl("jdbc:OrAcLe:foo", Provider.ORACLE);
+ }
+
+ @Test
+ public void resolve_MySql_when_jdbc_url_contains_mysql_in_any_case() {
+ checkProviderForUrlAndUnchangedUrl("jdbc:mysql:foo", Provider.MYSQL);
+
+ checkProviderForUrlAndUnchangedUrl("jdbc:mYsQL:foo", Provider.MYSQL);
+ }
+
+ @Test
+ public void resolve_SqlServer_when_jdbc_url_contains_sqlserver_in_any_case() {
+ checkProviderForUrlAndUnchangedUrl("jdbc:sqlserver:foo", Provider.SQLSERVER);
+
+ checkProviderForUrlAndUnchangedUrl("jdbc:SQLSeRVeR:foo", Provider.SQLSERVER);
+ }
+
+ @Test
+ public void resolve_POSTGRESQL_when_jdbc_url_contains_POSTGRESQL_in_any_case() {
+ checkProviderForUrlAndUnchangedUrl("jdbc:postgresql:foo", Provider.POSTGRESQL);
+
+ checkProviderForUrlAndUnchangedUrl("jdbc:POSTGRESQL:foo", Provider.POSTGRESQL);
+ }
+
+ private void checkProviderForUrlAndUnchangedUrl(String url, Provider expected) {
+ Props props = newProps(JDBC_URL, url);
+
+ assertThat(underTest.resolveProviderAndEnforceNonnullJdbcUrl(props)).isEqualTo(expected);
+ assertThat(props.nonNullValue(JDBC_URL)).isEqualTo(url);
+ }
+
+ @Test
+ public void fail_with_MessageException_when_provider_is_not_supported() {
+ Props props = newProps(JDBC_URL, "jdbc:microsoft:sqlserver://localhost");
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Unsupported JDBC driver provider: microsoft");
+
+ underTest.resolveProviderAndEnforceNonnullJdbcUrl(props);
+ }
+
+ @Test
+ public void fail_with_MessageException_when_url_does_not_have_jdbc_prefix() {
+ Props props = newProps(JDBC_URL, "oracle:thin:@localhost/XE");
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Bad format of JDBC URL: oracle:thin:@localhost/XE");
+
+ underTest.resolveProviderAndEnforceNonnullJdbcUrl(props);
+ }
+
+ @Test
+ public void check_mysql_parameters() {
+ // minimal -> ok
+ underTest.checkUrlParameters(Provider.MYSQL,
+ "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8");
+
+ // full -> ok
+ underTest.checkUrlParameters(Provider.MYSQL,
+ "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
+
+ // missing required -> ko
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("JDBC URL must have the property 'useUnicode=true'");
+
+ underTest.checkUrlParameters(Provider.MYSQL, "jdbc:mysql://localhost:3306/sonar?characterEncoding=utf8");
+ }
+
+ @Test
+ public void checkAndComplete_sets_driver_path_for_oracle() throws Exception {
+ File driverFile = new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc6.jar");
+ FileUtils.touch(driverFile);
+
+ Props props = newProps(JDBC_URL, "jdbc:oracle:thin:@localhost/XE");
+ underTest.accept(props);
+ assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
+ }
+
+ @Test
+ public void sets_driver_path_for_h2() throws Exception {
+ File driverFile = new File(homeDir, "lib/jdbc/h2/h2.jar");
+ FileUtils.touch(driverFile);
+
+ Props props = newProps(JDBC_URL, "jdbc:h2:tcp://localhost:9092/sonar");
+ underTest.accept(props);
+ assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
+ }
+
+ @Test
+ public void checkAndComplete_sets_driver_path_for_postgresql() throws Exception {
+ File driverFile = new File(homeDir, "lib/jdbc/postgresql/pg.jar");
+ FileUtils.touch(driverFile);
+
+ Props props = newProps(JDBC_URL, "jdbc:postgresql://localhost/sonar");
+ underTest.accept(props);
+ assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
+ }
+
+ @Test
+ public void checkAndComplete_sets_driver_path_for_mssql() throws Exception {
+ File driverFile = new File(homeDir, "lib/jdbc/mssql/sqljdbc4.jar");
+ FileUtils.touch(driverFile);
+
+ Props props = newProps(JDBC_URL, "jdbc:sqlserver://localhost/sonar;SelectMethod=Cursor");
+ underTest.accept(props);
+ assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
+ }
+
+ @Test
+ public void driver_file() throws Exception {
+ File driverFile = new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc6.jar");
+ FileUtils.touch(driverFile);
+
+ String path = underTest.driverPath(homeDir, Provider.ORACLE);
+ assertThat(path).isEqualTo(driverFile.getAbsolutePath());
+ }
+
+ @Test
+ public void driver_dir_does_not_exist() throws Exception {
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Directory does not exist: extensions/jdbc-driver/oracle");
+
+ underTest.driverPath(homeDir, Provider.ORACLE);
+ }
+
+ @Test
+ public void no_files_in_driver_dir() throws Exception {
+ FileUtils.forceMkdir(new File(homeDir, "extensions/jdbc-driver/oracle"));
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Directory does not contain JDBC driver: extensions/jdbc-driver/oracle");
+
+ underTest.driverPath(homeDir, Provider.ORACLE);
+ }
+
+ @Test
+ public void too_many_files_in_driver_dir() throws Exception {
+ FileUtils.touch(new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc5.jar"));
+ FileUtils.touch(new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc6.jar"));
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("Directory must contain only one JAR file: extensions/jdbc-driver/oracle");
+
+ underTest.driverPath(homeDir, Provider.ORACLE);
+ }
+
+ private Props newProps(String... params) {
+ Properties properties = new Properties();
+ for (int i = 0; i < params.length; i++) {
+ properties.setProperty(params[i], params[i + 1]);
+ i++;
+ }
+ properties.setProperty(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+ return new Props(properties);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.config;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SonarQubeVersionHelperTest {
+ @Test
+ public void getSonarQubeVersion_must_not_return_an_empty_string() {
+ assertThat(SonarQubeVersionHelper.getSonarqubeVersion()).isNotEmpty();
+ }
+
+ @Test
+ public void getSonarQubeVersion_must_always_return_same_value() {
+ String sonarqubeVersion = SonarQubeVersionHelper.getSonarqubeVersion();
+ for (int i = 0; i < 3; i++) {
+ assertThat(SonarQubeVersionHelper.getSonarqubeVersion()).isEqualTo(sonarqubeVersion);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+package org.sonar.application.config;
+
+import java.util.Optional;
+import java.util.Properties;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+/**
+ * Simple implementation of {@link AppSettings} that loads
+ * the default values defined by {@link ProcessProperties}.
+ */
+public class TestAppSettings implements AppSettings {
+
+ private Props props;
+
+ public TestAppSettings() {
+ this.props = new Props(new Properties());
+ ProcessProperties.completeDefaults(this.props);
+ }
+
+ public TestAppSettings set(String key, String value) {
+ this.props.set(key, value);
+ return this;
+ }
+
+ @Override
+ public Props getProps() {
+ return props;
+ }
+
+ @Override
+ public Optional<String> getValue(String key) {
+ return Optional.ofNullable(props.value(key));
+ }
+
+ @Override
+ public void reload(Props copy) {
+ this.props = copy;
+ }
+
+ public void clearProperty(String key) {
+ this.props.rawProperties().remove(key);
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Test;
+import org.sonar.application.process.Lifecycle;
+import org.sonar.application.process.ProcessLifecycleListener;
+import org.sonar.process.ProcessId;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.application.process.Lifecycle.State.INIT;
+import static org.sonar.application.process.Lifecycle.State.STARTED;
+import static org.sonar.application.process.Lifecycle.State.STARTING;
+import static org.sonar.application.process.Lifecycle.State.STOPPING;
+
+public class LifecycleTest {
+
+ @Test
+ public void initial_state_is_INIT() {
+ Lifecycle lifecycle = new Lifecycle(ProcessId.ELASTICSEARCH, Collections.emptyList());
+ assertThat(lifecycle.getState()).isEqualTo(INIT);
+ }
+
+ @Test
+ public void try_to_move_does_not_support_jumping_states() {
+ TestLifeCycleListener listener = new TestLifeCycleListener();
+ Lifecycle lifecycle = new Lifecycle(ProcessId.ELASTICSEARCH, asList(listener));
+ assertThat(lifecycle.getState()).isEqualTo(INIT);
+ assertThat(listener.states).isEmpty();
+
+ assertThat(lifecycle.tryToMoveTo(STARTED)).isFalse();
+ assertThat(lifecycle.getState()).isEqualTo(INIT);
+ assertThat(listener.states).isEmpty();
+
+ assertThat(lifecycle.tryToMoveTo(STARTING)).isTrue();
+ assertThat(lifecycle.getState()).isEqualTo(STARTING);
+ assertThat(listener.states).containsOnly(STARTING);
+ }
+
+ @Test
+ public void no_state_can_not_move_to_itself() {
+ for (Lifecycle.State state : Lifecycle.State.values()) {
+ assertThat(newLifeCycle(state).tryToMoveTo(state)).isFalse();
+ }
+ }
+
+ @Test
+ public void can_move_to_STOPPING_from_STARTING_STARTED_only() {
+ for (Lifecycle.State state : Lifecycle.State.values()) {
+ TestLifeCycleListener listener = new TestLifeCycleListener();
+ boolean tryToMoveTo = newLifeCycle(state, listener).tryToMoveTo(STOPPING);
+ if (state == STARTING || state == STARTED) {
+ assertThat(tryToMoveTo).as("from state " + state).isTrue();
+ assertThat(listener.states).containsOnly(STOPPING);
+ } else {
+ assertThat(tryToMoveTo).as("from state " + state).isFalse();
+ assertThat(listener.states).isEmpty();
+ }
+ }
+ }
+
+ @Test
+ public void can_move_to_STARTED_from_STARTING_only() {
+ for (Lifecycle.State state : Lifecycle.State.values()) {
+ TestLifeCycleListener listener = new TestLifeCycleListener();
+ boolean tryToMoveTo = newLifeCycle(state, listener).tryToMoveTo(STARTED);
+ if (state == STARTING) {
+ assertThat(tryToMoveTo).as("from state " + state).isTrue();
+ assertThat(listener.states).containsOnly(STARTED);
+ } else {
+ assertThat(tryToMoveTo).as("from state " + state).isFalse();
+ assertThat(listener.states).isEmpty();
+ }
+ }
+ }
+
+ private static Lifecycle newLifeCycle(Lifecycle.State state, TestLifeCycleListener... listeners) {
+ return new Lifecycle(ProcessId.ELASTICSEARCH, Arrays.asList(listeners), state);
+ }
+
+ private static final class TestLifeCycleListener implements ProcessLifecycleListener {
+ private final List<Lifecycle.State> states = new ArrayList<>();
+
+ @Override
+ public void onProcessState(ProcessId processId, Lifecycle.State state) {
+ this.states.add(state);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.concurrent.TimeUnit;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.process.sharedmemoryfile.ProcessCommands;
+import org.sonar.process.ProcessId;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+
+public class ProcessCommandsProcessMonitorTest {
+
+ @Test
+ public void ProcessMonitorImpl_is_a_proxy_of_Process() throws Exception {
+ Process process = mock(Process.class, RETURNS_DEEP_STUBS);
+ ProcessCommands commands = mock(ProcessCommands.class, RETURNS_DEEP_STUBS);
+
+ ProcessCommandsProcessMonitor underTest = new ProcessCommandsProcessMonitor(process, ProcessId.WEB_SERVER, commands);
+
+ underTest.waitFor();
+ verify(process).waitFor();
+
+ underTest.closeStreams();
+ verify(process.getErrorStream()).close();
+ verify(process.getInputStream()).close();
+ verify(process.getOutputStream()).close();
+
+ underTest.destroyForcibly();
+ verify(process).destroyForcibly();
+
+ assertThat(underTest.getInputStream()).isNotNull();
+
+ underTest.isAlive();
+ verify(process).isAlive();
+
+ underTest.waitFor(123, TimeUnit.MILLISECONDS);
+ verify(process).waitFor(123, TimeUnit.MILLISECONDS);
+ }
+
+ @Test
+ public void ProcessMonitorImpl_is_a_proxy_of_Commands() throws Exception {
+ Process process = mock(Process.class, RETURNS_DEEP_STUBS);
+ ProcessCommands commands = mock(ProcessCommands.class, RETURNS_DEEP_STUBS);
+
+ ProcessCommandsProcessMonitor underTest = new ProcessCommandsProcessMonitor(process, null, commands);
+
+ underTest.askForStop();
+ verify(commands).askForStop();
+
+ underTest.acknowledgeAskForRestart();
+ verify(commands).acknowledgeAskForRestart();
+
+ underTest.askedForRestart();
+ verify(commands).askedForRestart();
+
+ underTest.isOperational();
+ verify(commands).isOperational();
+ }
+
+ @Test
+ public void closeStreams_ignores_null_stream() {
+ ProcessCommands commands = mock(ProcessCommands.class);
+ Process process = mock(Process.class);
+ when(process.getInputStream()).thenReturn(null);
+
+ ProcessCommandsProcessMonitor underTest = new ProcessCommandsProcessMonitor(process, null, commands);
+
+ // no failures
+ underTest.closeStreams();
+ }
+
+ @Test
+ public void closeStreams_ignores_failure_if_stream_fails_to_be_closed() throws Exception {
+ InputStream stream = mock(InputStream.class);
+ doThrow(new IOException("error")).when(stream).close();
+ Process process = mock(Process.class);
+ when(process.getInputStream()).thenReturn(stream);
+
+ ProcessCommandsProcessMonitor underTest = new ProcessCommandsProcessMonitor(process, null, mock(ProcessCommands.class, Mockito.RETURNS_MOCKS));
+
+ // no failures
+ underTest.closeStreams();
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.process.ProcessId;
+import org.sonar.process.command.JavaCommand;
+import org.sonar.process.jmvoptions.JvmOptions;
+import org.sonar.process.sharedmemoryfile.AllProcessesCommands;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.data.MapEntry.entry;
+import static org.mockito.Mockito.RETURNS_MOCKS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ProcessLauncherImplTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private AllProcessesCommands commands = mock(AllProcessesCommands.class, RETURNS_MOCKS);
+
+ @Test
+ public void launch_forks_a_new_process() throws Exception {
+ File tempDir = temp.newFolder();
+ TestProcessBuilder processBuilder = new TestProcessBuilder();
+ ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder);
+ JavaCommand<JvmOptions> command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder());
+ command.addClasspath("lib/*.class");
+ command.addClasspath("lib/*.jar");
+ command.setArgument("foo", "bar");
+ command.setClassName("org.sonarqube.Main");
+ command.setEnvVariable("VAR1", "valueOfVar1");
+ command.setJvmOptions(new JvmOptions<>()
+ .add("-Dfoo=bar")
+ .add("-Dfoo2=bar2"));
+
+ ProcessMonitor monitor = underTest.launch(command);
+
+ assertThat(monitor).isNotNull();
+ assertThat(processBuilder.started).isTrue();
+ assertThat(processBuilder.commands.get(0)).endsWith("java");
+ assertThat(processBuilder.commands).containsSequence(
+ "-Dfoo=bar",
+ "-Dfoo2=bar2",
+ "-cp",
+ "lib/*.class" + System.getProperty("path.separator") + "lib/*.jar",
+ "org.sonarqube.Main");
+ assertThat(processBuilder.dir).isEqualTo(command.getWorkDir());
+ assertThat(processBuilder.redirectErrorStream).isTrue();
+ assertThat(processBuilder.environment)
+ .contains(entry("VAR1", "valueOfVar1"))
+ .containsAllEntriesOf(command.getEnvVariables());
+ }
+
+ @Test
+ public void properties_are_passed_to_command_via_a_temporary_properties_file() throws Exception {
+ File tempDir = temp.newFolder();
+ TestProcessBuilder processBuilder = new TestProcessBuilder();
+ ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder);
+ JavaCommand<JvmOptions> command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder());
+ command.setArgument("foo", "bar");
+ command.setArgument("baz", "woo");
+ command.setJvmOptions(new JvmOptions<>());
+
+ underTest.launch(command);
+
+ String propsFilePath = processBuilder.commands.get(processBuilder.commands.size() - 1);
+ File file = new File(propsFilePath);
+ assertThat(file).exists().isFile();
+ try (FileReader reader = new FileReader(file)) {
+ Properties props = new Properties();
+ props.load(reader);
+ assertThat(props).containsOnly(
+ entry("foo", "bar"),
+ entry("baz", "woo"),
+ entry("process.terminationTimeout", "60000"),
+ entry("process.key", ProcessId.ELASTICSEARCH.getKey()),
+ entry("process.index", String.valueOf(ProcessId.ELASTICSEARCH.getIpcIndex())),
+ entry("process.sharedDir", tempDir.getAbsolutePath()));
+ }
+ }
+
+ @Test
+ public void throw_ISE_if_command_fails() throws IOException {
+ File tempDir = temp.newFolder();
+ ProcessLauncherImpl.ProcessBuilder processBuilder = mock(ProcessLauncherImpl.ProcessBuilder.class, RETURNS_MOCKS);
+ when(processBuilder.start()).thenThrow(new IOException("error"));
+ ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder);
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Fail to launch process [es]");
+
+ underTest.launch(new JavaCommand(ProcessId.ELASTICSEARCH, temp.newFolder()));
+ }
+
+ private static class TestProcessBuilder implements ProcessLauncherImpl.ProcessBuilder {
+ private List<String> commands = null;
+ private File dir = null;
+ private Boolean redirectErrorStream = null;
+ private final Map<String, String> environment = new HashMap<>();
+ private boolean started = false;
+
+ @Override
+ public List<String> command() {
+ return commands;
+ }
+
+ @Override
+ public TestProcessBuilder command(List<String> commands) {
+ this.commands = commands;
+ return this;
+ }
+
+ @Override
+ public TestProcessBuilder directory(File dir) {
+ this.dir = dir;
+ return this;
+ }
+
+ @Override
+ public Map<String, String> environment() {
+ return environment;
+ }
+
+ @Override
+ public TestProcessBuilder redirectErrorStream(boolean b) {
+ this.redirectErrorStream = b;
+ return this;
+ }
+
+ @Override
+ public Process start() throws IOException {
+ this.started = true;
+ return mock(Process.class);
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.InputStream;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.mockito.Mockito;
+import org.sonar.process.ProcessId;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+public class SQProcessTest {
+
+ private static final ProcessId A_PROCESS_ID = ProcessId.ELASTICSEARCH;
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
+
+ @Test
+ public void initial_state_is_INIT() {
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
+
+ assertThat(underTest.getProcessId()).isEqualTo(A_PROCESS_ID);
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.INIT);
+ }
+
+ @Test
+ public void start_and_stop_process() {
+ ProcessLifecycleListener listener = mock(ProcessLifecycleListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addProcessLifecycleListener(listener)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ assertThat(underTest.start(() -> testProcess)).isTrue();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STARTED);
+ assertThat(testProcess.isAlive()).isTrue();
+ assertThat(testProcess.streamsClosed).isFalse();
+ verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STARTED);
+
+ testProcess.close();
+ // do not wait next run of watcher threads
+ underTest.refreshState();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ assertThat(testProcess.isAlive()).isFalse();
+ assertThat(testProcess.streamsClosed).isTrue();
+ verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPED);
+ }
+ }
+
+ @Test
+ public void start_does_not_nothing_if_already_started_once() {
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ assertThat(underTest.start(() -> testProcess)).isTrue();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STARTED);
+
+ assertThat(underTest.start(() -> {throw new IllegalStateException();})).isFalse();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STARTED);
+ }
+ }
+
+ @Test
+ public void start_throws_exception_and_move_to_state_STOPPED_if_execution_of_command_fails() {
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("error");
+
+ underTest.start(() -> {throw new IllegalStateException("error");});
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ }
+
+ @Test
+ public void send_event_when_process_is_operational() {
+ ProcessEventListener listener = mock(ProcessEventListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addEventListener(listener)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ testProcess.operational = true;
+ underTest.refreshState();
+
+ verify(listener).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.OPERATIONAL);
+ }
+ verifyNoMoreInteractions(listener);
+ }
+
+ @Test
+ public void operational_event_is_sent_once() {
+ ProcessEventListener listener = mock(ProcessEventListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addEventListener(listener)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+ testProcess.operational = true;
+
+ underTest.refreshState();
+ verify(listener).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.OPERATIONAL);
+
+ // second run
+ underTest.refreshState();
+ verifyNoMoreInteractions(listener);
+ }
+ }
+
+ @Test
+ public void send_event_when_process_requests_for_restart() {
+ ProcessEventListener listener = mock(ProcessEventListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addEventListener(listener)
+ .setWatcherDelayMs(1L)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ testProcess.askedForRestart = true;
+ verify(listener, timeout(10_000)).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.ASK_FOR_RESTART);
+
+ // flag is reset so that next run does not trigger again the event
+ underTest.refreshState();
+ verifyNoMoreInteractions(listener);
+ assertThat(testProcess.askedForRestart).isFalse();
+ }
+ }
+
+ @Test
+ public void stopForcibly_stops_the_process_without_graceful_request_for_stop() {
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ underTest.stopForcibly();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ assertThat(testProcess.askedForStop).isFalse();
+ assertThat(testProcess.destroyedForcibly).isTrue();
+
+ // second execution of stopForcibly does nothing. It's still stopped.
+ underTest.stopForcibly();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ }
+ }
+
+ @Test
+ public void process_stops_after_graceful_request_for_stop() throws Exception {
+ ProcessLifecycleListener listener = mock(ProcessLifecycleListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addProcessLifecycleListener(listener)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ Thread stopperThread = new Thread(() -> underTest.stop(1, TimeUnit.HOURS));
+ stopperThread.start();
+
+ // thread is blocked until process stopped
+ assertThat(stopperThread.isAlive()).isTrue();
+
+ // wait for the stopper thread to ask graceful stop
+ while (!testProcess.askedForStop) {
+ Thread.sleep(1L);
+ }
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPING);
+ verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPING);
+
+ // process stopped
+ testProcess.close();
+
+ // waiting for stopper thread to detect and handle the stop
+ stopperThread.join();
+
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPED);
+ }
+ }
+
+ @Test
+ public void process_is_stopped_forcibly_if_graceful_stop_is_too_long() throws Exception {
+ ProcessLifecycleListener listener = mock(ProcessLifecycleListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addProcessLifecycleListener(listener)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ underTest.stop(1L, TimeUnit.MILLISECONDS);
+
+ testProcess.waitFor();
+ assertThat(testProcess.askedForStop).isTrue();
+ assertThat(testProcess.destroyedForcibly).isTrue();
+ assertThat(testProcess.isAlive()).isFalse();
+ assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
+ verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPED);
+ }
+ }
+
+ @Test
+ public void process_requests_are_listened_on_regular_basis() throws Exception {
+ ProcessEventListener listener = mock(ProcessEventListener.class);
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
+ .addEventListener(listener)
+ .setWatcherDelayMs(1L)
+ .build();
+
+ try (TestProcess testProcess = new TestProcess()) {
+ underTest.start(() -> testProcess);
+
+ testProcess.operational = true;
+
+ verify(listener, timeout(1_000L)).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.OPERATIONAL);
+ }
+ }
+
+ @Test
+ public void test_toString() {
+ SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
+ assertThat(underTest.toString()).isEqualTo("Process[" + A_PROCESS_ID.getKey() + "]");
+ }
+
+ private static class TestProcess implements ProcessMonitor, AutoCloseable {
+
+ private final CountDownLatch alive = new CountDownLatch(1);
+ private final InputStream inputStream = mock(InputStream.class, Mockito.RETURNS_MOCKS);
+ private final InputStream errorStream = mock(InputStream.class, Mockito.RETURNS_MOCKS);
+ private boolean streamsClosed = false;
+ private boolean operational = false;
+ private boolean askedForRestart = false;
+ private boolean askedForStop = false;
+ private boolean destroyedForcibly = false;
+
+ @Override
+ public InputStream getInputStream() {
+ return inputStream;
+ }
+
+ @Override
+ public InputStream getErrorStream() {
+ return errorStream;
+ }
+
+ @Override
+ public void closeStreams() {
+ streamsClosed = true;
+ }
+
+ @Override
+ public boolean isAlive() {
+ return alive.getCount() == 1;
+ }
+
+ @Override
+ public void askForStop() {
+ askedForStop = true;
+ // do not stop, just asking
+ }
+
+ @Override
+ public void destroyForcibly() {
+ destroyedForcibly = true;
+ alive.countDown();
+ }
+
+ @Override
+ public void waitFor() throws InterruptedException {
+ alive.await();
+ }
+
+ @Override
+ public void waitFor(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
+ alive.await(timeout, timeoutUnit);
+ }
+
+ @Override
+ public boolean isOperational() {
+ return operational;
+ }
+
+ @Override
+ public boolean askedForRestart() {
+ return askedForRestart;
+ }
+
+ @Override
+ public void acknowledgeAskForRestart() {
+ this.askedForRestart = false;
+ }
+
+ @Override
+ public void close() {
+ alive.countDown();
+ }
+ }
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.DisableOnDebug;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.TestRule;
+import org.junit.rules.Timeout;
+import org.sonar.application.FileSystem;
+import org.sonar.application.Scheduler;
+import org.sonar.application.config.AppSettings;
+import org.sonar.process.sharedmemoryfile.ProcessCommands;
+import org.sonar.process.ProcessProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.timeout;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyZeroInteractions;
+import static org.mockito.Mockito.when;
+
+public class StopRequestWatcherImplTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
+
+ private AppSettings settings = mock(AppSettings.class, RETURNS_DEEP_STUBS);
+ private ProcessCommands commands = mock(ProcessCommands.class);
+ private Scheduler scheduler = mock(Scheduler.class);
+
+ @Test
+ public void do_not_watch_command_if_disabled() throws IOException {
+ enableSetting(false);
+ StopRequestWatcherImpl underTest = new StopRequestWatcherImpl(settings, scheduler, commands);
+
+ underTest.startWatching();
+ assertThat(underTest.isAlive()).isFalse();
+
+ underTest.stopWatching();
+ verifyZeroInteractions(commands, scheduler);
+ }
+
+ @Test
+ public void watch_stop_command_if_enabled() throws Exception {
+ enableSetting(true);
+ StopRequestWatcherImpl underTest = new StopRequestWatcherImpl(settings, scheduler, commands);
+ underTest.setDelayMs(1L);
+
+ underTest.startWatching();
+ assertThat(underTest.isAlive()).isTrue();
+ verify(scheduler, never()).terminate();
+
+ when(commands.askedForStop()).thenReturn(true);
+ verify(scheduler, timeout(1_000L)).terminate();
+
+ underTest.stopWatching();
+ while (underTest.isAlive()) {
+ Thread.sleep(1L);
+ }
+ }
+
+ @Test
+ public void create_instance_with_default_delay() throws IOException {
+ FileSystem fs = mock(FileSystem.class);
+ when(fs.getTempDir()).thenReturn(temp.newFolder());
+
+ StopRequestWatcherImpl underTest = StopRequestWatcherImpl.create(settings, scheduler, fs);
+
+ assertThat(underTest.getDelayMs()).isEqualTo(500L);
+ }
+
+ @Test
+ public void stop_watching_commands_if_thread_is_interrupted() throws Exception {
+ enableSetting(true);
+ StopRequestWatcherImpl underTest = new StopRequestWatcherImpl(settings, scheduler, commands);
+
+ underTest.startWatching();
+ underTest.interrupt();
+
+ while (underTest.isAlive()) {
+ Thread.sleep(1L);
+ }
+ assertThat(underTest.isAlive()).isFalse();
+ }
+
+ private void enableSetting(boolean b) {
+ when(settings.getProps().valueAsBoolean(ProcessProperties.ENABLE_STOP_COMMAND)).thenReturn(b);
+ }
+
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2017 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+package org.sonar.application.process;
+
+import java.io.InputStream;
+import org.apache.commons.io.IOUtils;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.sonar.application.process.StreamGobbler;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+import static org.mockito.Mockito.verifyZeroInteractions;
+
+public class StreamGobblerTest {
+
+ @Test
+ public void forward_stream_to_log() {
+ InputStream stream = IOUtils.toInputStream("one\nsecond log\nthird log\n");
+ Logger logger = mock(Logger.class);
+
+ StreamGobbler gobbler = new StreamGobbler(stream, "WEB", logger);
+ verifyZeroInteractions(logger);
+
+ gobbler.start();
+ StreamGobbler.waitUntilFinish(gobbler);
+
+ verify(logger).info("one");
+ verify(logger).info("second log");
+ verify(logger).info("third log");
+ verifyNoMoreInteractions(logger);
+ }
+}
--- /dev/null
+<?xml version="1.0" encoding="UTF-8" ?>
+<configuration debug="false">
+ <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
+
+ <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
+ <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
+ <pattern>
+ %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n
+ </pattern>
+ </encoder>
+ </appender>
+
+ <root>
+ <level value="INFO"/>
+ <appender-ref ref="CONSOLE"/>
+ </root>
+
+</configuration>
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0"
- xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.sonarsource.sonarqube</groupId>
- <artifactId>server</artifactId>
- <version>6.6-SNAPSHOT</version>
- <relativePath>../</relativePath>
- </parent>
-
- <artifactId>sonar-process-monitor</artifactId>
- <name>SonarQube :: Process Monitor</name>
-
- <properties>
- <!--
- version as stored in JAR and displayed in webapp. It is
- overridden on Travis when replacing SNAPSHOT version by
- build unique version, for instance "6.3.0.12345".
- -->
- <buildVersion>${project.version}</buildVersion>
- </properties>
-
- <dependencies>
- <dependency>
- <groupId>${project.groupId}</groupId>
- <artifactId>sonar-process</artifactId>
- <version>${project.version}</version>
- </dependency>
- <dependency>
- <groupId>org.slf4j</groupId>
- <artifactId>slf4j-api</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-to-slf4j</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-api</artifactId>
- </dependency>
- <dependency>
- <groupId>org.apache.logging.log4j</groupId>
- <artifactId>log4j-core</artifactId>
- </dependency>
- <dependency>
- <groupId>ch.qos.logback</groupId>
- <artifactId>logback-classic</artifactId>
- </dependency>
- <dependency>
- <groupId>com.hazelcast</groupId>
- <artifactId>hazelcast</artifactId>
- </dependency>
- <!--
- Required by our usage of Guava for clustering : CeWorkerFactoryImpl.getClusteredWorkerUUIDs()
- -->
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- </dependency>
- <dependency>
- <groupId>org.elasticsearch.client</groupId>
- <artifactId>transport</artifactId>
- </dependency>
-
- <dependency>
- <groupId>com.google.code.findbugs</groupId>
- <artifactId>jsr305</artifactId>
- <scope>provided</scope>
- </dependency>
-
- <dependency>
- <groupId>junit</groupId>
- <artifactId>junit</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.assertj</groupId>
- <artifactId>assertj-core</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>org.mockito</groupId>
- <artifactId>mockito-core</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
- <groupId>com.hazelcast</groupId>
- <artifactId>hazelcast-client</artifactId>
- <scope>test</scope>
- </dependency>
- </dependencies>
-
- <build>
- <resources>
- <resource>
- <!-- Used to resolve variables in file sonarqube-version.txt -->
- <directory>src/main/resources</directory>
- <filtering>true</filtering>
- </resource>
- </resources>
- </build>
-</project>
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.FileVisitOption;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.util.EnumSet;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.application.config.AppSettings;
-import org.sonar.process.sharedmemoryfile.AllProcessesCommands;
-
-import static java.lang.String.format;
-import static java.nio.file.FileVisitResult.CONTINUE;
-import static org.apache.commons.io.FileUtils.forceMkdir;
-import static org.sonar.process.FileUtils2.deleteDirectory;
-import static org.sonar.process.ProcessProperties.PATH_DATA;
-import static org.sonar.process.ProcessProperties.PATH_LOGS;
-import static org.sonar.process.ProcessProperties.PATH_TEMP;
-import static org.sonar.process.ProcessProperties.PATH_WEB;
-
-public class AppFileSystem implements FileSystem {
-
- private static final Logger LOG = LoggerFactory.getLogger(AppFileSystem.class);
- private static final EnumSet<FileVisitOption> FOLLOW_LINKS = EnumSet.of(FileVisitOption.FOLLOW_LINKS);
-
- private final AppSettings settings;
-
- public AppFileSystem(AppSettings settings) {
- this.settings = settings;
- }
-
- @Override
- public void reset() throws IOException {
- createDirectory(PATH_DATA);
- createDirectory(PATH_WEB);
- createDirectory(PATH_LOGS);
- File tempDir = createOrCleanTempDirectory(PATH_TEMP);
- try (AllProcessesCommands allProcessesCommands = new AllProcessesCommands(tempDir)) {
- allProcessesCommands.clean();
- }
- }
-
- @Override
- public File getTempDir() {
- return settings.getProps().nonNullValueAsFile(PATH_TEMP);
- }
-
- private boolean createDirectory(String propKey) throws IOException {
- File dir = settings.getProps().nonNullValueAsFile(propKey);
- if (dir.exists()) {
- ensureIsNotAFile(propKey, dir);
- return false;
- }
-
- forceMkdir(dir);
- ensureIsNotAFile(propKey, dir);
- return true;
- }
-
- private static void ensureIsNotAFile(String propKey, File dir) {
- if (!dir.isDirectory()) {
- throw new IllegalStateException(format("Property '%s' is not valid, not a directory: %s",
- propKey, dir.getAbsolutePath()));
- }
- }
-
- private File createOrCleanTempDirectory(String propKey) throws IOException {
- File dir = settings.getProps().nonNullValueAsFile(propKey);
- LOG.info("Cleaning or creating temp directory {}", dir.getAbsolutePath());
- if (!createDirectory(propKey)) {
- Files.walkFileTree(dir.toPath(), FOLLOW_LINKS, CleanTempDirFileVisitor.VISIT_MAX_DEPTH, new CleanTempDirFileVisitor(dir.toPath()));
- }
- return dir;
- }
-
- private static class CleanTempDirFileVisitor extends SimpleFileVisitor<Path> {
- private static final Path SHAREDMEMORY_FILE = Paths.get("sharedmemory");
- static final int VISIT_MAX_DEPTH = 1;
-
- private final Path path;
- private final boolean symLink;
-
- CleanTempDirFileVisitor(Path path) {
- this.path = path;
- this.symLink = Files.isSymbolicLink(path);
- }
-
- @Override
- public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs) throws IOException {
- File file = filePath.toFile();
- if (file.isDirectory()) {
- deleteDirectory(file);
- } else if (filePath.getFileName().equals(SHAREDMEMORY_FILE)) {
- return CONTINUE;
- } else if (!symLink || !filePath.equals(path)) {
- Files.delete(filePath);
- }
- return CONTINUE;
- }
-
- @Override
- public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
- if (!dir.equals(path)) {
- deleteDirectory(dir.toFile());
- }
- return CONTINUE;
- }
- }
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import ch.qos.logback.classic.Level;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.ConsoleAppender;
-import ch.qos.logback.core.FileAppender;
-import org.sonar.application.config.AppSettings;
-import org.sonar.application.process.StreamGobbler;
-import org.sonar.process.ProcessId;
-import org.sonar.process.ProcessProperties;
-import org.sonar.process.logging.LogLevelConfig;
-import org.sonar.process.logging.LogbackHelper;
-import org.sonar.process.logging.RootLoggerConfig;
-
-import static org.slf4j.Logger.ROOT_LOGGER_NAME;
-import static org.sonar.application.process.StreamGobbler.LOGGER_GOBBLER;
-import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder;
-
-/**
- * Configure logback for the APP process.
- *
- * <p>
- * SonarQube's logging use cases:
- * <ol>
- * <li>
- * SQ started as a background process (with {@code sonar.sh start}):
- * <ul>
- * <li>
- * logs produced by the JVM before logback is setup in the APP JVM or which can't be caught by logback
- * (such as JVM crash) must be written to sonar.log
- * </li>
- * <li>
- * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught
- * by logback (such as JVM crash) must be written to sonar.log
- * </li>
- * <li>each JVM writes its own logs into its dedicated file</li>
- * </ul>
- * </li>
- * <li>
- * SQ started in console with wrapper (ie. with {@code sonar.sh console}):
- * <ul>
- * <li>
- * logs produced by the APP JVM before logback is setup in the APP JVM or which can't be caught by logback
- * (such as JVM crash) must be written to sonar.log
- * </li>
- * <li>
- * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught
- * by logback (such as JVM crash) must be written to sonar.log
- * </li>
- * <li>each JVM writes its own logs into its dedicated file</li>
- * <li>APP JVM logs are written to the APP JVM {@code System.out}</li>
- * </ul>
- * </li>
- * <li>
- * SQ started from command line (ie. {@code java -jar sonar-application-X.Y.jar}):
- * <ul>
- * <li>
- * logs produced by the APP JVM before logback is setup in the APP JVM or which can't be caught by logback
- * (such as JVM crash) are the responsibility of the user to be dealt with
- * </li>
- * <li>
- * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught
- * by logback (such as JVM crash) must be written to APP's {@code System.out}
- * </li>
- * <li>each JVM writes its own logs into its dedicated file</li>
- * <li>APP JVM logs are written to the APP JVM {@code System.out}</li>
- * </ul>
- * </li>
- * <li>
- * SQ started from an IT (ie. from command line with {@code option -Dsonar.log.console=true}):
- * <ul>
- * <li>
- * logs produced by the APP JVM before logback is setup in the APP JVM or which can't be caught by logback
- * (such as JVM crash) are the responsibility of the developer or maven to be dealt with
- * </li>
- * <li>
- * logs produced by the sub process JVMs before logback is setup in the subprocess JVMs or which can't be caught
- * by logback (such as JVM crash) must be written to APP's {@code System.out} and are the responsibility of the
- * developer or maven to be dealt with
- * </li>
- * <li>each JVM writes its own logs into its dedicated file</li>
- * <li>logs of all 4 JVMs are also written to the APP JVM {@code System.out}</li>
- * </ul>
- * </li>
- * </ol>
- * </p>
- *
- */
-public class AppLogging {
-
- private static final String CONSOLE_LOGGER = "console";
- private static final String CONSOLE_PLAIN_APPENDER = "CONSOLE";
- private static final String APP_CONSOLE_APPENDER = "APP_CONSOLE";
- private static final String GOBBLER_PLAIN_CONSOLE = "GOBBLER_CONSOLE";
- private static final RootLoggerConfig APP_ROOT_LOGGER_CONFIG = newRootLoggerConfigBuilder()
- .setProcessId(ProcessId.APP)
- .build();
-
- private final LogbackHelper helper = new LogbackHelper();
- private final AppSettings appSettings;
-
- public AppLogging(AppSettings appSettings) {
- this.appSettings = appSettings;
- }
-
- public LoggerContext configure() {
- LoggerContext ctx = helper.getRootContext();
- ctx.reset();
-
- helper.enableJulChangePropagation(ctx);
-
- configureConsole(ctx);
- if (helper.isAllLogsToConsoleEnabled(appSettings.getProps()) || !appSettings.getProps().valueAsBoolean("sonar.wrapped", false)) {
- configureWithLogbackWritingToFile(ctx);
- } else {
- configureWithWrapperWritingToFile(ctx);
- }
- helper.apply(
- LogLevelConfig.newBuilder(helper.getRootLoggerName())
- .rootLevelFor(ProcessId.APP)
- .immutableLevel("com.hazelcast",
- Level.toLevel(
- appSettings.getProps().nonNullValue(ProcessProperties.HAZELCAST_LOG_LEVEL)))
- .build(),
- appSettings.getProps());
-
- return ctx;
- }
-
- /**
- * Creates a non additive logger dedicated to printing message as is (ie. assuming they are already formatted).
- *
- * It creates a dedicated appender to the System.out which applies no formatting the logs it receives.
- */
- private void configureConsole(LoggerContext loggerContext) {
- ConsoleAppender<ILoggingEvent> consoleAppender = helper.newConsoleAppender(loggerContext, CONSOLE_PLAIN_APPENDER, "%msg%n");
-
- Logger consoleLogger = loggerContext.getLogger(CONSOLE_LOGGER);
- consoleLogger.setAdditive(false);
- consoleLogger.addAppender(consoleAppender);
- }
-
- /**
- * The process has been started by orchestrator (ie. via {@code java -jar} and optionally passing the option {@code -Dsonar.log.console=true}).
- * Therefor, APP's System.out (and System.err) are <strong>not</strong> copied to sonar.log by the wrapper and
- * printing to sonar.log must be done at logback level.
- */
- private void configureWithLogbackWritingToFile(LoggerContext ctx) {
- // configure all logs (ie. root logger) to be written to sonar.log and also to the console with formatting
- // in practice, this will be only APP's own logs as logs from sub processes LOGGER_GOBBLER and LOGGER_GOBBLER
- // is configured below to be detached from root
- // so, this will make all APP's log to be both written to sonar.log and visible in the console
- configureRootWithLogbackWritingToFile(ctx);
-
- // if option -Dsonar.log.console=true has been set, sub processes will write their logs to their own files but also
- // copy them to their System.out.
- // otherwise, the only logs to be expected in LOGGER_GOBBLER are those before logback is setup in subprocesses or
- // when their JVM crashes
- // they must be printed to App's System.out as is (as they are already formatted)
- // logger is configured to be non additive as we don't want these logs to be written to sonar.log and duplicated in
- // the console (with an incorrect formatting)
- configureGobbler(ctx);
- }
-
- /**
- * SQ has been started by the wrapper (ie. with sonar.sh) therefor, APP's System.out (and System.err) are written to
- * sonar.log by the wrapper.
- */
- private void configureWithWrapperWritingToFile(LoggerContext ctx) {
- // configure all logs (ie. root logger) to be written to console with formatting
- // in practice, this will be only APP's own logs as logs from sub processes are written to LOGGER_GOBBLER and
- // LOGGER_GOBBLER is configured below to be detached from root
- // logs are written to the console because we want them to be in sonar.log and the wrapper will write any log
- // from APP's System.out and System.err to sonar.log
- Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
- rootLogger.addAppender(createAppConsoleAppender(ctx, helper.buildLogPattern(APP_ROOT_LOGGER_CONFIG)));
-
- // in regular configuration, sub processes are not copying their logs to their System.out, so, the only logs to be
- // expected in LOGGER_GOBBLER are those before logback is setup in subprocesses or when JVM crashes
- // so, they must be printed to App's System.out as is (as they are already formatted) and the wrapper will write
- // them to sonar.log
- // logger is configured to be non additive as we don't want these logs written to sonar.log and duplicated in the
- // console with an incorrect formatting
- configureGobbler(ctx);
- }
-
- private void configureRootWithLogbackWritingToFile(LoggerContext ctx) {
- Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
- String appLogPattern = helper.buildLogPattern(APP_ROOT_LOGGER_CONFIG);
- FileAppender<ILoggingEvent> fileAppender = helper.newFileAppender(ctx, appSettings.getProps(), APP_ROOT_LOGGER_CONFIG, appLogPattern);
- rootLogger.addAppender(fileAppender);
- rootLogger.addAppender(createAppConsoleAppender(ctx, appLogPattern));
- }
-
- /**
- * Configure the logger to which logs from sub processes are written to
- * (called {@link StreamGobbler#LOGGER_GOBBLER}) by {@link StreamGobbler},
- * to be:
- * <ol>
- * <li>non additive (ie. these logs will be output by the appender of {@link StreamGobbler#LOGGER_GOBBLER} and only this one)</li>
- * <li>write logs as is (ie. without any extra formatting)</li>
- * <li>write exclusively to App's System.out</li>
- * </ol>
- */
- private void configureGobbler(LoggerContext ctx) {
- Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
- gobblerLogger.setAdditive(false);
- gobblerLogger.addAppender(helper.newConsoleAppender(ctx, GOBBLER_PLAIN_CONSOLE, "%msg%n"));
- }
-
- private ConsoleAppender<ILoggingEvent> createAppConsoleAppender(LoggerContext ctx, String appLogPattern) {
- return helper.newConsoleAppender(ctx, APP_CONSOLE_APPENDER, appLogPattern);
- }
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.io.IOException;
-import org.sonar.application.config.AppSettings;
-
-/**
- * Reload settings, reset logging and file system when a
- * server restart has been requested.
- */
-public interface AppReloader {
-
- /**
- * This method is called when server is down.
- */
- void reload(AppSettings settings) throws IOException;
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.io.IOException;
-import java.util.Objects;
-import org.sonar.application.config.AppSettings;
-import org.sonar.application.config.AppSettingsLoader;
-import org.sonar.application.config.ClusterSettings;
-import org.sonar.process.MessageException;
-import org.sonar.process.Props;
-
-import static java.lang.String.format;
-import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
-import static org.sonar.process.ProcessProperties.PATH_DATA;
-import static org.sonar.process.ProcessProperties.PATH_LOGS;
-import static org.sonar.process.ProcessProperties.PATH_TEMP;
-import static org.sonar.process.ProcessProperties.PATH_WEB;
-
-public class AppReloaderImpl implements AppReloader {
-
- private final AppSettingsLoader settingsLoader;
- private final FileSystem fileSystem;
- private final AppState appState;
- private final AppLogging logging;
-
- public AppReloaderImpl(AppSettingsLoader settingsLoader, FileSystem fileSystem, AppState appState, AppLogging logging) {
- this.settingsLoader = settingsLoader;
- this.fileSystem = fileSystem;
- this.appState = appState;
- this.logging = logging;
- }
-
- @Override
- public void reload(AppSettings settings) throws IOException {
- if (ClusterSettings.isClusterEnabled(settings)) {
- throw new IllegalStateException("Restart is not possible with cluster mode");
- }
- AppSettings reloaded = settingsLoader.load();
- ensureUnchangedConfiguration(settings.getProps(), reloaded.getProps());
- settings.reload(reloaded.getProps());
-
- fileSystem.reset();
- logging.configure();
- appState.reset();
- }
-
- private static void ensureUnchangedConfiguration(Props oldProps, Props newProps) {
- verifyUnchanged(oldProps, newProps, PATH_DATA);
- verifyUnchanged(oldProps, newProps, PATH_WEB);
- verifyUnchanged(oldProps, newProps, PATH_LOGS);
- verifyUnchanged(oldProps, newProps, PATH_TEMP);
- verifyUnchanged(oldProps, newProps, CLUSTER_ENABLED);
- }
-
- private static void verifyUnchanged(Props initialProps, Props newProps, String propKey) {
- String initialValue = initialProps.nonNullValue(propKey);
- String newValue = newProps.nonNullValue(propKey);
- if (!Objects.equals(initialValue, newValue)) {
- throw new MessageException(format("Property [%s] cannot be changed on restart: [%s] => [%s]", propKey, initialValue, newValue));
- }
- }
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.util.Optional;
-import org.sonar.process.ProcessId;
-
-public interface AppState extends AutoCloseable {
-
- void addListener(AppStateListener listener);
-
- /**
- * Whether the process with the specified {@code processId}
- * has been marked as operational.
- *
- * If parameter {@code local} is {@code true}, then only the
- * process on the local node is requested.
- *
- * If parameter {@code local} is {@code false}, then only
- * the processes on remote nodes are requested, excluding
- * the local node. In this case at least one process must
- * be marked as operational.
- */
- boolean isOperational(ProcessId processId, boolean local);
-
- /**
- * Mark local process as operational. In cluster mode, this
- * event is propagated to all nodes.
- */
- void setOperational(ProcessId processId);
-
- boolean tryToLockWebLeader();
-
- void reset();
-
- void registerSonarQubeVersion(String sonarqubeVersion);
-
- Optional<String> getLeaderHostName();
-
- @Override
- void close();
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import org.sonar.application.cluster.AppStateClusterImpl;
-import org.sonar.application.config.AppSettings;
-import org.sonar.application.config.ClusterSettings;
-
-public class AppStateFactory {
-
- private final AppSettings settings;
-
- public AppStateFactory(AppSettings settings) {
- this.settings = settings;
- }
-
- public AppState create() {
- return ClusterSettings.isClusterEnabled(settings) ? new AppStateClusterImpl(settings) : new AppStateImpl();
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.util.ArrayList;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
-import javax.annotation.Nonnull;
-import org.sonar.process.NetworkUtils;
-import org.sonar.process.ProcessId;
-
-public class AppStateImpl implements AppState {
-
- private final Map<ProcessId, Boolean> processes = new EnumMap<>(ProcessId.class);
- private final List<AppStateListener> listeners = new ArrayList<>();
- private final AtomicBoolean webLeaderLocked = new AtomicBoolean(false);
-
- @Override
- public void addListener(@Nonnull AppStateListener listener) {
- this.listeners.add(listener);
- }
-
- @Override
- public boolean isOperational(ProcessId processId, boolean local) {
- return processes.computeIfAbsent(processId, p -> false);
- }
-
- @Override
- public void setOperational(ProcessId processId) {
- processes.put(processId, true);
- listeners.forEach(l -> l.onAppStateOperational(processId));
- }
-
- @Override
- public boolean tryToLockWebLeader() {
- return webLeaderLocked.compareAndSet(false, true);
- }
-
- @Override
- public void reset() {
- webLeaderLocked.set(false);
- processes.clear();
- }
-
- @Override
- public void registerSonarQubeVersion(String sonarqubeVersion) {
- // Nothing to do on non clustered version
- }
-
- @Override
- public Optional<String> getLeaderHostName() {
- return Optional.of(NetworkUtils.getHostName());
- }
-
- @Override
- public void close() {
- // nothing to do
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import org.sonar.process.ProcessId;
-
-@FunctionalInterface
-public interface AppStateListener {
-
- /**
- * The method is called when the state is changed. When cluster
- * mode is enabled, the event may be raised from another node.
- *
- * Listener must subscribe to {@link AppState#addListener(AppStateListener)}.
- */
- void onAppStateOperational(ProcessId processId);
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.io.File;
-import java.io.IOException;
-
-public interface FileSystem {
-
- void reset() throws IOException;
-
- File getTempDir();
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.EnumSet;
-import java.util.Map;
-import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import static org.sonar.application.NodeLifecycle.State.INIT;
-import static org.sonar.application.NodeLifecycle.State.OPERATIONAL;
-import static org.sonar.application.NodeLifecycle.State.STARTING;
-import static org.sonar.application.NodeLifecycle.State.STOPPED;
-import static org.sonar.application.NodeLifecycle.State.STOPPING;
-
-/**
- * Lifecycle of the cluster node, consolidating the states
- * of child processes.
- */
-class NodeLifecycle {
- private static final Logger LOG = LoggerFactory.getLogger(NodeLifecycle.class);
-
- enum State {
- // initial state, does nothing
- INIT,
-
- // at least one process is still starting
- STARTING,
-
- // all the processes are started and operational
- OPERATIONAL,
-
- // at least one process is still stopping
- STOPPING,
-
- // all processes are stopped
- STOPPED
- }
-
- private static final Map<State, Set<State>> TRANSITIONS = buildTransitions();
-
- private State state = INIT;
-
- private static Map<State, Set<State>> buildTransitions() {
- Map<State, Set<State>> res = new EnumMap<>(State.class);
- res.put(INIT, toSet(STARTING));
- res.put(STARTING, toSet(OPERATIONAL, STOPPING, STOPPED));
- res.put(OPERATIONAL, toSet(STOPPING, STOPPED));
- res.put(STOPPING, toSet(STOPPED));
- res.put(STOPPED, toSet(STARTING));
- return res;
- }
-
- private static Set<State> toSet(State... states) {
- if (states.length == 0) {
- return Collections.emptySet();
- }
- if (states.length == 1) {
- return Collections.singleton(states[0]);
- }
- return EnumSet.copyOf(Arrays.asList(states));
- }
-
- State getState() {
- return state;
- }
-
- synchronized boolean tryToMoveTo(State to) {
- boolean res = false;
- State currentState = state;
- if (TRANSITIONS.get(currentState).contains(to)) {
- this.state = to;
- res = true;
- }
- LOG.trace("tryToMoveTo from {} to {} => {}", currentState, to, res);
- return res;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-public interface Scheduler {
-
- void schedule();
-
- /**
- * Stops all processes and waits for them to be down.
- */
- void terminate();
-
- /**
- * Blocks until all processes are down
- */
- void awaitTermination();
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.util.EnumMap;
-import java.util.Optional;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Supplier;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.application.config.AppSettings;
-import org.sonar.application.config.ClusterSettings;
-import org.sonar.process.command.CommandFactory;
-import org.sonar.process.command.EsCommand;
-import org.sonar.process.command.JavaCommand;
-import org.sonar.application.process.ProcessLauncher;
-import org.sonar.application.process.Lifecycle;
-import org.sonar.application.process.ProcessEventListener;
-import org.sonar.application.process.ProcessLifecycleListener;
-import org.sonar.application.process.ProcessMonitor;
-import org.sonar.application.process.SQProcess;
-import org.sonar.process.ProcessId;
-
-public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLifecycleListener, AppStateListener {
-
- private static final Logger LOG = LoggerFactory.getLogger(SchedulerImpl.class);
-
- private final AppSettings settings;
- private final AppReloader appReloader;
- private final CommandFactory commandFactory;
- private final ProcessLauncher processLauncher;
- private final AppState appState;
- private final NodeLifecycle nodeLifecycle = new NodeLifecycle();
-
- private final CountDownLatch keepAlive = new CountDownLatch(1);
- private final AtomicBoolean restartRequested = new AtomicBoolean(false);
- private final AtomicBoolean restartDisabled = new AtomicBoolean(false);
- private final EnumMap<ProcessId, SQProcess> processesById = new EnumMap<>(ProcessId.class);
- private final AtomicInteger operationalCountDown = new AtomicInteger();
- private final AtomicInteger stopCountDown = new AtomicInteger(0);
- private StopperThread stopperThread;
- private RestarterThread restarterThread;
- private long processWatcherDelayMs = SQProcess.DEFAULT_WATCHER_DELAY_MS;
-
- public SchedulerImpl(AppSettings settings, AppReloader appReloader, CommandFactory commandFactory,
- ProcessLauncher processLauncher,
- AppState appState) {
- this.settings = settings;
- this.appReloader = appReloader;
- this.commandFactory = commandFactory;
- this.processLauncher = processLauncher;
- this.appState = appState;
- this.appState.addListener(this);
- }
-
- SchedulerImpl setProcessWatcherDelayMs(long l) {
- this.processWatcherDelayMs = l;
- return this;
- }
-
- @Override
- public void schedule() {
- if (!nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STARTING)) {
- return;
- }
- processesById.clear();
-
- for (ProcessId processId : ClusterSettings.getEnabledProcesses(settings)) {
- SQProcess process = SQProcess.builder(processId)
- .addProcessLifecycleListener(this)
- .addEventListener(this)
- .setWatcherDelayMs(processWatcherDelayMs)
- .build();
- processesById.put(process.getProcessId(), process);
- }
- operationalCountDown.set(processesById.size());
-
- tryToStartAll();
- }
-
- private void tryToStartAll() {
- tryToStartEs();
- tryToStartWeb();
- tryToStartCe();
- }
-
- private void tryToStartEs() {
- SQProcess process = processesById.get(ProcessId.ELASTICSEARCH);
- if (process != null) {
- tryToStartEsProcess(process, commandFactory::createEsCommand);
- }
- }
-
- private void tryToStartWeb() {
- SQProcess process = processesById.get(ProcessId.WEB_SERVER);
- if (process == null || !isEsClientStartable()) {
- return;
- }
- if (appState.isOperational(ProcessId.WEB_SERVER, false)) {
- tryToStartJavaProcess(process, () -> commandFactory.createWebCommand(false));
- } else if (appState.tryToLockWebLeader()) {
- tryToStartJavaProcess(process, () -> commandFactory.createWebCommand(true));
- } else {
- Optional<String> leader = appState.getLeaderHostName();
- if (leader.isPresent()) {
- LOG.info("Waiting for initialization from " + leader.get());
- } else {
- LOG.error("Initialization failed. All nodes must be restarted");
- }
- }
- }
-
- private void tryToStartCe() {
- SQProcess process = processesById.get(ProcessId.COMPUTE_ENGINE);
- if (process != null && appState.isOperational(ProcessId.WEB_SERVER, false) && isEsClientStartable()) {
- tryToStartJavaProcess(process, commandFactory::createCeCommand);
- }
- }
-
- private boolean isEsClientStartable() {
- boolean requireLocalEs = ClusterSettings.isLocalElasticsearchEnabled(settings);
- return appState.isOperational(ProcessId.ELASTICSEARCH, requireLocalEs);
- }
-
- private void tryToStartJavaProcess(SQProcess process, Supplier<JavaCommand> commandSupplier) {
- tryToStart(process, () -> {
- JavaCommand command = commandSupplier.get();
- return processLauncher.launch(command);
- });
- }
-
- private void tryToStartEsProcess(SQProcess process, Supplier<EsCommand> commandSupplier) {
- tryToStart(process, () -> {
- EsCommand command = commandSupplier.get();
- return processLauncher.launch(command);
- });
- }
-
- private void tryToStart(SQProcess process, Supplier<ProcessMonitor> processMonitorSupplier) {
- try {
- process.start(processMonitorSupplier);
- } catch (RuntimeException e) {
- // failed to start command -> stop everything
- terminate();
- throw e;
- }
- }
-
- private void stopAll() {
- // order is important for non-cluster mode
- stopProcess(ProcessId.COMPUTE_ENGINE);
- stopProcess(ProcessId.WEB_SERVER);
- stopProcess(ProcessId.ELASTICSEARCH);
- }
-
- /**
- * Request for graceful stop then blocks until process is stopped.
- * Returns immediately if the process is disabled in configuration.
- */
- private void stopProcess(ProcessId processId) {
- SQProcess process = processesById.get(processId);
- if (process != null) {
- process.stop(1, TimeUnit.MINUTES);
- }
- }
-
- /**
- * Blocks until all processes are stopped. Pending restart, if
- * any, is disabled.
- */
- @Override
- public void terminate() {
- // disable ability to request for restart
- restartRequested.set(false);
- restartDisabled.set(true);
-
- if (nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STOPPING)) {
- LOG.info("Stopping SonarQube");
- }
- stopAll();
- if (stopperThread != null) {
- stopperThread.interrupt();
- }
- if (restarterThread != null) {
- restarterThread.interrupt();
- }
- keepAlive.countDown();
- }
-
- @Override
- public void awaitTermination() {
- try {
- keepAlive.await();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
-
- @Override
- public void onProcessEvent(ProcessId processId, Type type) {
- if (type == Type.OPERATIONAL) {
- onProcessOperational(processId);
- } else if (type == Type.ASK_FOR_RESTART && restartRequested.compareAndSet(false, true)) {
- stopAsync();
- }
- }
-
- private void onProcessOperational(ProcessId processId) {
- LOG.info("Process[{}] is up", processId.getKey());
- appState.setOperational(processId);
- if (operationalCountDown.decrementAndGet() == 0 && nodeLifecycle.tryToMoveTo(NodeLifecycle.State.OPERATIONAL)) {
- LOG.info("SonarQube is up");
- }
- }
-
- @Override
- public void onAppStateOperational(ProcessId processId) {
- if (nodeLifecycle.getState() == NodeLifecycle.State.STARTING) {
- tryToStartAll();
- }
- }
-
- @Override
- public void onProcessState(ProcessId processId, Lifecycle.State to) {
- switch (to) {
- case STOPPED:
- onProcessStop(processId);
- break;
- case STARTING:
- stopCountDown.incrementAndGet();
- break;
- default:
- // Nothing to do
- break;
- }
- }
-
- private void onProcessStop(ProcessId processId) {
- LOG.info("Process [{}] is stopped", processId.getKey());
- if (stopCountDown.decrementAndGet() == 0 && nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STOPPED)) {
- if (!restartDisabled.get() &&
- restartRequested.compareAndSet(true, false)) {
- LOG.info("SonarQube is restarting");
- restartAsync();
- } else {
- LOG.info("SonarQube is stopped");
- // all processes are stopped, no restart requested
- // Let's clean-up resources
- terminate();
- }
-
- } else if (nodeLifecycle.tryToMoveTo(NodeLifecycle.State.STOPPING)) {
- // this is the first process stopping
- stopAsync();
- }
- }
-
- private void stopAsync() {
- stopperThread = new StopperThread();
- stopperThread.start();
- }
-
- private void restartAsync() {
- restarterThread = new RestarterThread();
- restarterThread.start();
- }
-
- private class RestarterThread extends Thread {
- public RestarterThread() {
- super("Restarter");
- }
-
- @Override
- public void run() {
- try {
- appReloader.reload(settings);
- schedule();
- } catch (Exception e) {
- LOG.error("Fail to restart", e);
- terminate();
- }
- }
- }
-
- private class StopperThread extends Thread {
- public StopperThread() {
- super("Stopper");
- }
-
- @Override
- public void run() {
- stopAll();
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.cluster;
-
-import java.util.EnumMap;
-import java.util.Map;
-import java.util.Optional;
-import java.util.stream.Collectors;
-import javax.annotation.Nonnull;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.application.AppState;
-import org.sonar.application.AppStateListener;
-import org.sonar.application.config.AppSettings;
-import org.sonar.process.ProcessId;
-import org.sonar.process.ProcessProperties;
-
-public class AppStateClusterImpl implements AppState {
- private static Logger LOGGER = LoggerFactory.getLogger(AppStateClusterImpl.class);
-
- private final Map<ProcessId, Boolean> localProcesses = new EnumMap<>(ProcessId.class);
- private final HazelcastCluster hazelcastCluster;
-
- public AppStateClusterImpl(AppSettings appSettings) {
- ClusterProperties clusterProperties = new ClusterProperties(appSettings);
- clusterProperties.validate();
-
- if (!clusterProperties.isEnabled()) {
- throw new IllegalStateException("Cluster is not enabled on this instance");
- }
-
- hazelcastCluster = HazelcastCluster.create(clusterProperties);
- // Add the local endpoint to be used by processes
- appSettings.getProps().set(ProcessProperties.CLUSTER_LOCALENDPOINT, hazelcastCluster.getLocalEndPoint());
- appSettings.getProps().set(ProcessProperties.CLUSTER_MEMBERUUID, hazelcastCluster.getLocalUUID());
-
- String members = hazelcastCluster.getMembers().stream().collect(Collectors.joining(","));
- LOGGER.info("Joined the cluster [{}] that contains the following hosts : [{}]", hazelcastCluster.getName(), members);
- }
-
- @Override
- public void addListener(@Nonnull AppStateListener listener) {
- hazelcastCluster.addListener(listener);
- }
-
- @Override
- public boolean isOperational(@Nonnull ProcessId processId, boolean local) {
- if (local) {
- return localProcesses.computeIfAbsent(processId, p -> false);
- }
- return hazelcastCluster.isOperational(processId);
- }
-
- @Override
- public void setOperational(@Nonnull ProcessId processId) {
- localProcesses.put(processId, true);
- hazelcastCluster.setOperational(processId);
- }
-
- @Override
- public boolean tryToLockWebLeader() {
- return hazelcastCluster.tryToLockWebLeader();
- }
-
- @Override
- public void reset() {
- throw new IllegalStateException("state reset is not supported in cluster mode");
- }
-
- @Override
- public void close() {
- hazelcastCluster.close();
- }
-
- @Override
- public void registerSonarQubeVersion(String sonarqubeVersion) {
- hazelcastCluster.registerSonarQubeVersion(sonarqubeVersion);
- }
-
- @Override
- public Optional<String> getLeaderHostName() {
- return hazelcastCluster.getLeaderHostName();
- }
-
- HazelcastCluster getHazelcastCluster() {
- return hazelcastCluster;
- }
-
- /**
- * Only used for testing purpose
- *
- * @param logger
- */
- static void setLogger(Logger logger) {
- AppStateClusterImpl.LOGGER = logger;
- }
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.cluster;
-
-import java.io.Serializable;
-import org.sonar.process.ProcessId;
-
-import static java.util.Objects.requireNonNull;
-
-public class ClusterProcess implements Serializable {
- private final ProcessId processId;
- private final String nodeUuid;
-
- public ClusterProcess(String nodeUuid, ProcessId processId) {
- this.processId = requireNonNull(processId);
- this.nodeUuid = requireNonNull(nodeUuid);
- }
-
- public ProcessId getProcessId() {
- return processId;
- }
-
- public String getNodeUuid() {
- return nodeUuid;
- }
-
- @Override
- public boolean equals(Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- ClusterProcess that = (ClusterProcess) o;
- if (processId != that.processId) {
- return false;
- }
- return nodeUuid.equals(that.nodeUuid);
- }
-
- @Override
- public int hashCode() {
- int result = processId.hashCode();
- result = 31 * result + nodeUuid.hashCode();
- return result;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.cluster;
-
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.util.ArrayList;
-import java.util.Enumeration;
-import java.util.List;
-import javax.annotation.Nullable;
-import org.apache.commons.lang.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.application.config.AppSettings;
-import org.sonar.process.ProcessProperties;
-
-/**
- * Properties of the cluster configuration
- */
-public final class ClusterProperties {
- static final String DEFAULT_PORT = "9003";
- private static final Logger LOGGER = LoggerFactory.getLogger(ClusterProperties.class);
-
- private final int port;
- private final boolean enabled;
- private final List<String> hosts;
- private final List<String> networkInterfaces;
- private final String name;
-
- ClusterProperties(AppSettings appSettings) {
- port = appSettings.getProps().valueAsInt(ProcessProperties.CLUSTER_PORT);
- enabled = appSettings.getProps().valueAsBoolean(ProcessProperties.CLUSTER_ENABLED);
- networkInterfaces = extractNetworkInterfaces(
- appSettings.getProps().value(ProcessProperties.CLUSTER_NETWORK_INTERFACES, "")
- );
- name = appSettings.getProps().nonNullValue(ProcessProperties.CLUSTER_NAME);
- hosts = extractHosts(
- appSettings.getProps().value(ProcessProperties.CLUSTER_HOSTS, "")
- );
- }
-
- int getPort() {
- return port;
- }
-
- boolean isEnabled() {
- return enabled;
- }
-
- List<String> getHosts() {
- return hosts;
- }
-
- List<String> getNetworkInterfaces() {
- return networkInterfaces;
- }
-
- String getName() {
- return name;
- }
-
- void validate() {
- if (!enabled) {
- return;
- }
-
- // Test validity of port
- checkArgument(
- port > 0 && port < 65_536,
- "Cluster port have been set to %d which is outside the range [1-65535].",
- port
- );
-
- // Test the networkInterfaces parameter
- try {
- List<String> localInterfaces = findAllLocalIPs();
-
- networkInterfaces.forEach(
- inet -> checkArgument(
- StringUtils.isEmpty(inet) || localInterfaces.contains(inet),
- "Interface %s is not available on this machine.",
- inet
- )
- );
- } catch (SocketException e) {
- LOGGER.warn("Unable to retrieve network networkInterfaces. Interfaces won't be checked", e);
- }
- }
-
- private static List<String> extractHosts(final String hosts) {
- List<String> result = new ArrayList<>();
- for (String host : hosts.split(",")) {
- if (StringUtils.isNotEmpty(host)) {
- if (!host.contains(":")) {
- result.add(
- String.format("%s:%s", host, DEFAULT_PORT)
- );
- } else {
- result.add(host);
- }
- }
- }
- return result;
- }
-
- private static List<String> extractNetworkInterfaces(final String networkInterfaces) {
- List<String> result = new ArrayList<>();
- for (String iface : networkInterfaces.split(",")) {
- if (StringUtils.isNotEmpty(iface)) {
- result.add(iface);
- }
- }
- return result;
- }
-
- private static List<String> findAllLocalIPs() throws SocketException {
- Enumeration<NetworkInterface> netInterfaces = NetworkInterface.getNetworkInterfaces();
- List<String> localInterfaces = new ArrayList<>();
-
- while (netInterfaces.hasMoreElements()) {
- NetworkInterface networkInterface = netInterfaces.nextElement();
- Enumeration<InetAddress> ips = networkInterface.getInetAddresses();
- while (ips.hasMoreElements()) {
- InetAddress ip = ips.nextElement();
- localInterfaces.add(ip.getHostAddress());
- }
- }
- return localInterfaces;
- }
-
- private static void checkArgument(boolean expression,
- @Nullable String messageTemplate,
- @Nullable Object... args) {
- if (!expression) {
- throw new IllegalArgumentException(String.format(messageTemplate, args));
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.cluster;
-
-import com.hazelcast.config.Config;
-import com.hazelcast.config.JoinConfig;
-import com.hazelcast.config.NetworkConfig;
-import com.hazelcast.core.Client;
-import com.hazelcast.core.ClientListener;
-import com.hazelcast.core.EntryEvent;
-import com.hazelcast.core.EntryListener;
-import com.hazelcast.core.Hazelcast;
-import com.hazelcast.core.HazelcastInstance;
-import com.hazelcast.core.HazelcastInstanceNotActiveException;
-import com.hazelcast.core.IAtomicReference;
-import com.hazelcast.core.ILock;
-import com.hazelcast.core.MapEvent;
-import com.hazelcast.core.Member;
-import com.hazelcast.core.ReplicatedMap;
-import com.hazelcast.nio.Address;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.application.AppStateListener;
-import org.sonar.process.ProcessId;
-
-import static java.util.stream.Collectors.toList;
-import static org.sonar.process.NetworkUtils.getHostName;
-import static org.sonar.process.cluster.ClusterObjectKeys.CLIENT_UUIDS;
-import static org.sonar.process.cluster.ClusterObjectKeys.HOSTNAME;
-import static org.sonar.process.cluster.ClusterObjectKeys.LEADER;
-import static org.sonar.process.cluster.ClusterObjectKeys.OPERATIONAL_PROCESSES;
-import static org.sonar.process.cluster.ClusterObjectKeys.SONARQUBE_VERSION;
-
-public class HazelcastCluster implements AutoCloseable {
- private static final Logger LOGGER = LoggerFactory.getLogger(HazelcastCluster.class);
-
- private final List<AppStateListener> listeners = new ArrayList<>();
- private final ReplicatedMap<ClusterProcess, Boolean> operationalProcesses;
- private final String operationalProcessListenerUUID;
- private final String clientListenerUUID;
-
- protected final HazelcastInstance hzInstance;
-
- private HazelcastCluster(Config hzConfig) {
- // Create the Hazelcast instance
- hzInstance = Hazelcast.newHazelcastInstance(hzConfig);
-
- // Get or create the replicated map
- operationalProcesses = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
- operationalProcessListenerUUID = operationalProcesses.addEntryListener(new OperationalProcessListener());
- clientListenerUUID = hzInstance.getClientService().addClientListener(new ConnectedClientListener());
- }
-
- String getLocalUUID() {
- return hzInstance.getLocalEndpoint().getUuid();
- }
-
- String getName() {
- return hzInstance.getConfig().getGroupConfig().getName();
- }
-
- List<String> getMembers() {
- return hzInstance.getCluster().getMembers().stream()
- .filter(m -> !m.localMember())
- .map(m -> m.getStringAttribute(HOSTNAME))
- .collect(toList());
- }
-
- void addListener(AppStateListener listener) {
- listeners.add(listener);
- }
-
- boolean isOperational(ProcessId processId) {
- for (Map.Entry<ClusterProcess, Boolean> entry : operationalProcesses.entrySet()) {
- if (entry.getKey().getProcessId().equals(processId) && entry.getValue()) {
- return true;
- }
- }
- return false;
- }
-
- void setOperational(ProcessId processId) {
- operationalProcesses.put(new ClusterProcess(getLocalUUID(), processId), Boolean.TRUE);
- }
-
- boolean tryToLockWebLeader() {
- IAtomicReference<String> leader = hzInstance.getAtomicReference(LEADER);
- if (leader.get() == null) {
- ILock lock = hzInstance.getLock(LEADER);
- lock.lock();
- try {
- if (leader.get() == null) {
- leader.set(getLocalUUID());
- return true;
- } else {
- return false;
- }
- } finally {
- lock.unlock();
- }
- } else {
- return false;
- }
- }
-
- public void registerSonarQubeVersion(String sonarqubeVersion) {
- IAtomicReference<String> sqVersion = hzInstance.getAtomicReference(SONARQUBE_VERSION);
- if (sqVersion.get() == null) {
- ILock lock = hzInstance.getLock(SONARQUBE_VERSION);
- lock.lock();
- try {
- if (sqVersion.get() == null) {
- sqVersion.set(sonarqubeVersion);
- }
- } finally {
- lock.unlock();
- }
- }
-
- String clusterVersion = sqVersion.get();
- if (!sqVersion.get().equals(sonarqubeVersion)) {
- throw new IllegalStateException(
- String.format("The local version %s is not the same as the cluster %s", sonarqubeVersion, clusterVersion)
- );
- }
- }
-
- @Override
- public void close() {
- if (hzInstance != null) {
- try {
- // Removing listeners
- operationalProcesses.removeEntryListener(operationalProcessListenerUUID);
- hzInstance.getClientService().removeClientListener(clientListenerUUID);
-
- // Removing the operationalProcess from the replicated map
- operationalProcesses.keySet().forEach(
- clusterNodeProcess -> {
- if (clusterNodeProcess.getNodeUuid().equals(getLocalUUID())) {
- operationalProcesses.remove(clusterNodeProcess);
- }
- });
-
- // Shutdown Hazelcast properly
- hzInstance.shutdown();
- } catch (HazelcastInstanceNotActiveException e) {
- // hazelcastCluster may be already closed by the shutdown hook
- LOGGER.debug("Unable to close Hazelcast cluster", e);
- }
- }
- }
-
- public static HazelcastCluster create(ClusterProperties clusterProperties) {
- Config hzConfig = new Config();
- hzConfig.getGroupConfig().setName(clusterProperties.getName());
-
- // Configure the network instance
- NetworkConfig netConfig = hzConfig.getNetworkConfig();
- netConfig
- .setPort(clusterProperties.getPort())
- .setReuseAddress(true);
-
- if (!clusterProperties.getNetworkInterfaces().isEmpty()) {
- netConfig.getInterfaces()
- .setEnabled(true)
- .setInterfaces(clusterProperties.getNetworkInterfaces());
- }
-
- // Only allowing TCP/IP configuration
- JoinConfig joinConfig = netConfig.getJoin();
- joinConfig.getAwsConfig().setEnabled(false);
- joinConfig.getMulticastConfig().setEnabled(false);
- joinConfig.getTcpIpConfig().setEnabled(true);
- joinConfig.getTcpIpConfig().setMembers(clusterProperties.getHosts());
-
- // Tweak HazelCast configuration
- hzConfig
- // Increase the number of tries
- .setProperty("hazelcast.tcp.join.port.try.count", "10")
- // Don't bind on all interfaces
- .setProperty("hazelcast.socket.bind.any", "false")
- // Don't phone home
- .setProperty("hazelcast.phone.home.enabled", "false")
- // Use slf4j for logging
- .setProperty("hazelcast.logging.type", "slf4j");
-
- // Trying to resolve the hostname
- hzConfig.getMemberAttributeConfig().setStringAttribute(HOSTNAME, getHostName());
-
- // We are not using the partition group of Hazelcast, so disabling it
- hzConfig.getPartitionGroupConfig().setEnabled(false);
- return new HazelcastCluster(hzConfig);
- }
-
- Optional<String> getLeaderHostName() {
- String leaderId = (String) hzInstance.getAtomicReference(LEADER).get();
- if (leaderId != null) {
- Optional<Member> leader = hzInstance.getCluster().getMembers().stream().filter(m -> m.getUuid().equals(leaderId)).findFirst();
- if (leader.isPresent()) {
- return Optional.of(leader.get().getStringAttribute(HOSTNAME));
- }
- }
- return Optional.empty();
- }
-
- String getLocalEndPoint() {
- Address localAddress = hzInstance.getCluster().getLocalMember().getAddress();
- return String.format("%s:%d", localAddress.getHost(), localAddress.getPort());
- }
-
- private class OperationalProcessListener implements EntryListener<ClusterProcess, Boolean> {
- @Override
- public void entryAdded(EntryEvent<ClusterProcess, Boolean> event) {
- if (event.getValue()) {
- listeners.forEach(appStateListener -> appStateListener.onAppStateOperational(event.getKey().getProcessId()));
- }
- }
-
- @Override
- public void entryRemoved(EntryEvent<ClusterProcess, Boolean> event) {
- // Ignore it
- }
-
- @Override
- public void entryUpdated(EntryEvent<ClusterProcess, Boolean> event) {
- if (event.getValue()) {
- listeners.forEach(appStateListener -> appStateListener.onAppStateOperational(event.getKey().getProcessId()));
- }
- }
-
- @Override
- public void entryEvicted(EntryEvent<ClusterProcess, Boolean> event) {
- // Ignore it
- }
-
- @Override
- public void mapCleared(MapEvent event) {
- // Ignore it
- }
-
- @Override
- public void mapEvicted(MapEvent event) {
- // Ignore it
- }
- }
-
- private class ConnectedClientListener implements ClientListener {
- @Override
- public void clientConnected(Client client) {
- hzInstance.getSet(CLIENT_UUIDS).add(client.getUuid());
- }
-
- @Override
- public void clientDisconnected(Client client) {
- hzInstance.getSet(CLIENT_UUIDS).remove(client.getUuid());
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.application.cluster;
-
-import javax.annotation.ParametersAreNonnullByDefault;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import java.util.Optional;
-import org.sonar.process.Props;
-
-public interface AppSettings {
-
- Props getProps();
-
- Optional<String> getValue(String key);
-
- void reload(Props copy);
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import java.util.Optional;
-import org.sonar.process.Props;
-
-public class AppSettingsImpl implements AppSettings {
-
- private Props props;
-
- AppSettingsImpl(Props props) {
- this.props = props;
- }
-
- @Override
- public Props getProps() {
- return props;
- }
-
- @Override
- public Optional<String> getValue(String key) {
- return Optional.ofNullable(props.value(key));
- }
-
- @Override
- public void reload(Props copy) {
- this.props = copy;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-public interface AppSettingsLoader {
-
- AppSettings load();
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.io.Reader;
-import java.net.URISyntaxException;
-import java.util.Arrays;
-import java.util.Properties;
-import java.util.function.Consumer;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.ConfigurationUtils;
-import org.sonar.process.ProcessProperties;
-import org.sonar.process.Props;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-public class AppSettingsLoaderImpl implements AppSettingsLoader {
-
- private final File homeDir;
- private final String[] cliArguments;
- private final Consumer<Props>[] consumers;
-
- public AppSettingsLoaderImpl(String[] cliArguments) {
- this(cliArguments, detectHomeDir(), new FileSystemSettings(), new JdbcSettings(), new ClusterSettings());
- }
-
- AppSettingsLoaderImpl(String[] cliArguments, File homeDir, Consumer<Props>... consumers) {
- this.cliArguments = cliArguments;
- this.homeDir = homeDir;
- this.consumers = consumers;
- }
-
- File getHomeDir() {
- return homeDir;
- }
-
- @Override
- public AppSettings load() {
- Properties p = loadPropertiesFile(homeDir);
- p.putAll(CommandLineParser.parseArguments(cliArguments));
- p.setProperty(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
- p = ConfigurationUtils.interpolateVariables(p, System.getenv());
-
- // the difference between Properties and Props is that the latter
- // supports decryption of values, so it must be used when values
- // are accessed
- Props props = new Props(p);
- ProcessProperties.completeDefaults(props);
- Arrays.stream(consumers).forEach(c -> c.accept(props));
-
- return new AppSettingsImpl(props);
- }
-
- private static File detectHomeDir() {
- try {
- File appJar = new File(AppSettingsLoaderImpl.class.getProtectionDomain().getCodeSource().getLocation().toURI());
- return appJar.getParentFile().getParentFile();
- } catch (URISyntaxException e) {
- throw new IllegalStateException("Cannot detect path of main jar file", e);
- }
- }
-
- /**
- * Loads the configuration file ${homeDir}/conf/sonar.properties.
- * An empty {@link Properties} is returned if the file does not exist.
- */
- private static Properties loadPropertiesFile(File homeDir) {
- Properties p = new Properties();
- File propsFile = new File(homeDir, "conf/sonar.properties");
- if (propsFile.exists()) {
- try (Reader reader = new InputStreamReader(new FileInputStream(propsFile), UTF_8)) {
- p.load(reader);
- } catch (IOException e) {
- throw new IllegalStateException("Cannot open file " + propsFile, e);
- }
- } else {
- LoggerFactory.getLogger(AppSettingsLoaderImpl.class).warn("Configuration file not found: {}", propsFile);
- }
- return p;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.net.HostAndPort;
-import com.google.common.net.InetAddresses;
-import java.net.InetAddress;
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.net.UnknownHostException;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.function.Consumer;
-import org.apache.commons.lang.StringUtils;
-import org.sonar.process.MessageException;
-import org.sonar.process.ProcessId;
-import org.sonar.process.ProcessProperties;
-import org.sonar.process.Props;
-
-import static com.google.common.net.InetAddresses.forString;
-import static java.lang.String.format;
-import static java.util.Arrays.stream;
-import static org.apache.commons.lang.StringUtils.isBlank;
-import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
-import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
-import static org.sonar.process.ProcessProperties.CLUSTER_NETWORK_INTERFACES;
-import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
-import static org.sonar.process.ProcessProperties.CLUSTER_WEB_LEADER;
-import static org.sonar.process.ProcessProperties.JDBC_URL;
-import static org.sonar.process.ProcessProperties.SEARCH_HOST;
-
-public class ClusterSettings implements Consumer<Props> {
-
- @Override
- public void accept(Props props) {
- if (!isClusterEnabled(props)) {
- return;
- }
-
- checkProperties(props);
- }
-
- private static void checkProperties(Props props) {
- // Cluster web leader is not allowed
- if (props.value(CLUSTER_WEB_LEADER) != null) {
- throw new MessageException(format("Property [%s] is forbidden", CLUSTER_WEB_LEADER));
- }
-
- if (props.valueAsBoolean(ProcessProperties.CLUSTER_ENABLED) &&
- !props.valueAsBoolean(ProcessProperties.CLUSTER_SEARCH_DISABLED, false)
- ) {
- ensureMandatoryProperty(props, SEARCH_HOST);
- ensureNotLoopback(props, SEARCH_HOST);
- }
- // Mandatory properties
- ensureMandatoryProperty(props, CLUSTER_HOSTS);
- ensureMandatoryProperty(props, CLUSTER_SEARCH_HOSTS);
-
- // H2 Database is not allowed in cluster mode
- String jdbcUrl = props.value(JDBC_URL);
- if (isBlank(jdbcUrl) || jdbcUrl.startsWith("jdbc:h2:")) {
- throw new MessageException("Embedded database is not supported in cluster mode");
- }
-
- // Loopback interfaces are forbidden for SEARCH_HOST and CLUSTER_NETWORK_INTERFACES
- ensureNotLoopback(props, CLUSTER_HOSTS);
- ensureNotLoopback(props, CLUSTER_NETWORK_INTERFACES);
- ensureNotLoopback(props, CLUSTER_SEARCH_HOSTS);
-
- ensureLocalAddress(props, SEARCH_HOST);
- ensureLocalAddress(props, CLUSTER_NETWORK_INTERFACES);
- }
-
- private static void ensureMandatoryProperty(Props props, String key) {
- if (isBlank(props.value(key))) {
- throw new MessageException(format("Property [%s] is mandatory", key));
- }
- }
-
- @VisibleForTesting
- protected static void ensureNotLoopback(Props props, String key) {
- String ipList = props.value(key);
- if (ipList == null) {
- return;
- }
-
- stream(ipList.split(","))
- .filter(StringUtils::isNotBlank)
- .map(StringUtils::trim)
- .forEach(ip -> {
- InetAddress inetAddress = convertToInetAddress(ip, key);
- if (inetAddress.isLoopbackAddress()) {
- throw new MessageException(format("The interface address [%s] of [%s] must not be a loopback address", ip, key));
- }
- });
- }
-
- private static void ensureLocalAddress(Props props, String key) {
- String ipList = props.value(key);
-
- if (ipList == null) {
- return;
- }
-
- stream(ipList.split(","))
- .filter(StringUtils::isNotBlank)
- .map(StringUtils::trim)
- .forEach(ip -> {
- InetAddress inetAddress = convertToInetAddress(ip, key);
- try {
- if (NetworkInterface.getByInetAddress(inetAddress) == null) {
- throw new MessageException(format("The interface address [%s] of [%s] is not a local address", ip, key));
- }
- } catch (SocketException e) {
- throw new MessageException(format("The interface address [%s] of [%s] is not a local address", ip, key));
- }
- });
- }
-
- private static InetAddress convertToInetAddress(String text, String key) {
- InetAddress inetAddress;
- HostAndPort hostAndPort = HostAndPort.fromString(text);
- if (!InetAddresses.isInetAddress(hostAndPort.getHostText())) {
- try {
- inetAddress =InetAddress.getByName(hostAndPort.getHostText());
- } catch (UnknownHostException e) {
- throw new MessageException(format("The interface address [%s] of [%s] cannot be resolved : %s", text, key, e.getMessage()));
- }
- } else {
- inetAddress = forString(hostAndPort.getHostText());
- }
-
- return inetAddress;
- }
-
- public static boolean isClusterEnabled(AppSettings settings) {
- return isClusterEnabled(settings.getProps());
- }
-
- private static boolean isClusterEnabled(Props props) {
- return props.valueAsBoolean(CLUSTER_ENABLED);
- }
-
- public static List<ProcessId> getEnabledProcesses(AppSettings settings) {
- if (!isClusterEnabled(settings)) {
- return Arrays.asList(ProcessId.ELASTICSEARCH, ProcessId.WEB_SERVER, ProcessId.COMPUTE_ENGINE);
- }
- List<ProcessId> enabled = new ArrayList<>();
- if (!settings.getProps().valueAsBoolean(ProcessProperties.CLUSTER_SEARCH_DISABLED)) {
- enabled.add(ProcessId.ELASTICSEARCH);
- }
- if (!settings.getProps().valueAsBoolean(ProcessProperties.CLUSTER_WEB_DISABLED)) {
- enabled.add(ProcessId.WEB_SERVER);
- }
-
- if (!settings.getProps().valueAsBoolean(ProcessProperties.CLUSTER_CE_DISABLED)) {
- enabled.add(ProcessId.COMPUTE_ENGINE);
- }
- return enabled;
- }
-
- public static boolean isLocalElasticsearchEnabled(AppSettings settings) {
- return !isClusterEnabled(settings.getProps()) ||
- !settings.getProps().valueAsBoolean(ProcessProperties.CLUSTER_SEARCH_DISABLED);
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import org.apache.commons.lang.StringUtils;
-
-import java.util.Map;
-import java.util.Properties;
-
-public class CommandLineParser {
-
- private CommandLineParser() {
- // prevent instantiation
- }
-
- /**
- * Build properties from command-line arguments and system properties
- */
- public static Properties parseArguments(String[] args) {
- Properties props = argumentsToProperties(args);
-
- // complete with only the system properties that start with "sonar."
- for (Map.Entry<Object, Object> entry : System.getProperties().entrySet()) {
- String key = entry.getKey().toString();
- if (key.startsWith("sonar.")) {
- props.setProperty(key, entry.getValue().toString());
- }
- }
- return props;
- }
-
- /**
- * Convert strings "-Dkey=value" to properties
- */
- static Properties argumentsToProperties(String[] args) {
- Properties props = new Properties();
- for (String arg : args) {
- if (!arg.startsWith("-D") || !arg.contains("=")) {
- throw new IllegalArgumentException(String.format(
- "Command-line argument must start with -D, for example -Dsonar.jdbc.username=sonar. Got: %s", arg));
- }
- String key = StringUtils.substringBefore(arg, "=").substring(2);
- String value = StringUtils.substringAfter(arg, "=");
- props.setProperty(key, value);
- }
- return props;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import java.io.File;
-import java.util.function.Consumer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.Props;
-
-import static org.sonar.process.ProcessProperties.PATH_DATA;
-import static org.sonar.process.ProcessProperties.PATH_HOME;
-import static org.sonar.process.ProcessProperties.PATH_LOGS;
-import static org.sonar.process.ProcessProperties.PATH_TEMP;
-import static org.sonar.process.ProcessProperties.PATH_WEB;
-
-public class FileSystemSettings implements Consumer<Props> {
-
- private static final Logger LOG = LoggerFactory.getLogger(FileSystemSettings.class);
-
- @Override
- public void accept(Props props) {
- ensurePropertyIsAbsolutePath(props, PATH_DATA);
- ensurePropertyIsAbsolutePath(props, PATH_WEB);
- ensurePropertyIsAbsolutePath(props, PATH_LOGS);
- ensurePropertyIsAbsolutePath(props, PATH_TEMP);
- }
-
- private static File ensurePropertyIsAbsolutePath(Props props, String propKey) {
- File homeDir = props.nonNullValueAsFile(PATH_HOME);
- // default values are set by ProcessProperties
- String path = props.nonNullValue(propKey);
- File d = new File(path);
- if (!d.isAbsolute()) {
- d = new File(homeDir, path);
- LOG.trace("Overriding property {} from relative path '{}' to absolute path '{}'", path, d.getAbsolutePath());
- props.set(propKey, d.getAbsolutePath());
- }
- return d;
- }
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import java.io.File;
-import java.net.Inet6Address;
-import java.net.InetAddress;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.function.Consumer;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import org.apache.commons.io.FileUtils;
-import org.apache.commons.lang.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.MessageException;
-import org.sonar.process.ProcessProperties;
-import org.sonar.process.Props;
-
-import static java.lang.String.format;
-import static org.apache.commons.lang.StringUtils.isEmpty;
-import static org.apache.commons.lang.StringUtils.isNotEmpty;
-import static org.sonar.process.ProcessProperties.JDBC_EMBEDDED_PORT;
-import static org.sonar.process.ProcessProperties.JDBC_URL;
-
-public class JdbcSettings implements Consumer<Props> {
-
- private static final int JDBC_EMBEDDED_PORT_DEFAULT_VALUE = 9092;
-
- enum Provider {
- H2("lib/jdbc/h2"), SQLSERVER("lib/jdbc/mssql"), MYSQL("lib/jdbc/mysql"), ORACLE("extensions/jdbc-driver/oracle"),
- POSTGRESQL("lib/jdbc/postgresql");
-
- final String path;
-
- Provider(String path) {
- this.path = path;
- }
- }
-
- @Override
- public void accept(Props props) {
- File homeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME);
- Provider provider = resolveProviderAndEnforceNonnullJdbcUrl(props);
- String url = props.value(JDBC_URL);
- checkUrlParameters(provider, url);
- String driverPath = driverPath(homeDir, provider);
- props.set(ProcessProperties.JDBC_DRIVER_PATH, driverPath);
- }
-
- String driverPath(File homeDir, Provider provider) {
- String dirPath = provider.path;
- File dir = new File(homeDir, dirPath);
- if (!dir.exists()) {
- throw new MessageException("Directory does not exist: " + dirPath);
- }
- List<File> files = new ArrayList<>(FileUtils.listFiles(dir, new String[] {"jar"}, false));
- if (files.isEmpty()) {
- throw new MessageException("Directory does not contain JDBC driver: " + dirPath);
- }
- if (files.size() > 1) {
- throw new MessageException("Directory must contain only one JAR file: " + dirPath);
- }
- return files.get(0).getAbsolutePath();
- }
-
- Provider resolveProviderAndEnforceNonnullJdbcUrl(Props props) {
- String url = props.value(JDBC_URL);
- Integer embeddedDatabasePort = props.valueAsInt(JDBC_EMBEDDED_PORT);
-
- if (embeddedDatabasePort != null) {
- String correctUrl = buildH2JdbcUrl(embeddedDatabasePort);
- warnIfUrlIsSet(embeddedDatabasePort, url, correctUrl);
- props.set(JDBC_URL, correctUrl);
- return Provider.H2;
- }
-
- if (isEmpty(url)) {
- props.set(JDBC_URL, buildH2JdbcUrl(JDBC_EMBEDDED_PORT_DEFAULT_VALUE));
- props.set(JDBC_EMBEDDED_PORT, String.valueOf(JDBC_EMBEDDED_PORT_DEFAULT_VALUE));
- return Provider.H2;
- }
-
- Pattern pattern = Pattern.compile("jdbc:(\\w+):.+");
- Matcher matcher = pattern.matcher(url);
- if (!matcher.find()) {
- throw new MessageException(format("Bad format of JDBC URL: %s", url));
- }
- String key = matcher.group(1);
- try {
- return Provider.valueOf(StringUtils.upperCase(key));
- } catch (IllegalArgumentException e) {
- throw new MessageException(format("Unsupported JDBC driver provider: %s", key));
- }
- }
-
- private static String buildH2JdbcUrl(int embeddedDatabasePort) {
- InetAddress ip = InetAddress.getLoopbackAddress();
- String host;
- if (ip instanceof Inet6Address) {
- host = "[" + ip.getHostAddress() + "]";
- } else {
- host = ip.getHostAddress();
- }
- return format("jdbc:h2:tcp://%s:%d/sonar", host, embeddedDatabasePort);
- }
-
- void checkUrlParameters(Provider provider, String url) {
- if (Provider.MYSQL.equals(provider)) {
- checkRequiredParameter(url, "useUnicode=true");
- checkRequiredParameter(url, "characterEncoding=utf8");
- checkRecommendedParameter(url, "rewriteBatchedStatements=true");
- checkRecommendedParameter(url, "useConfigs=maxPerformance");
- }
- }
-
- private static void warnIfUrlIsSet(int port, String existing, String expectedUrl) {
- if (isNotEmpty(existing)) {
- Logger logger = LoggerFactory.getLogger(JdbcSettings.class);
- if (expectedUrl.equals(existing)) {
- logger.warn("To change H2 database port, only property '{}' should be set (which current value is '{}'). " +
- "Remove property '{}' from configuration to remove this warning.",
- JDBC_EMBEDDED_PORT, port,
- JDBC_URL);
- } else {
- logger.warn("Both '{}' and '{}' properties are set. " +
- "The value of property '{}' ('{}') is not consistent with the value of property '{}' ('{}'). " +
- "The value of property '{}' will be ignored and value '{}' will be used instead. " +
- "To remove this warning, either remove property '{}' if your intent was to use the embedded H2 database, otherwise remove property '{}'.",
- JDBC_EMBEDDED_PORT, JDBC_URL,
- JDBC_URL, existing, JDBC_EMBEDDED_PORT, port,
- JDBC_URL, expectedUrl,
- JDBC_URL, JDBC_EMBEDDED_PORT);
- }
- }
- }
-
- private static void checkRequiredParameter(String url, String val) {
- if (!url.contains(val)) {
- throw new MessageException(format("JDBC URL must have the property '%s'", val));
- }
- }
-
- private void checkRecommendedParameter(String url, String val) {
- if (!url.contains(val)) {
- LoggerFactory.getLogger(getClass()).warn("JDBC URL is recommended to have the property '{}'", val);
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.config;
-
-import java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-
-import static java.lang.String.format;
-
-public class SonarQubeVersionHelper {
- private static final String SONARQUBE_VERSION_PATH = "/sonarqube-version.txt";
-
- private static String sonarqubeVersion;
-
- private SonarQubeVersionHelper() {
- // only static methods
- }
-
- public static String getSonarqubeVersion() {
- if (sonarqubeVersion == null) {
- loadVersion();
- }
- return sonarqubeVersion;
- }
-
- private static synchronized void loadVersion() {
- try {
- try (BufferedReader in = new BufferedReader(
- new InputStreamReader(
- SonarQubeVersionHelper.class.getResourceAsStream(SONARQUBE_VERSION_PATH),
- StandardCharsets.UTF_8
- ))) {
- sonarqubeVersion = in.readLine();
- }
- } catch (IOException e) {
- throw new IllegalStateException(format("Cannot load %s from classpath", SONARQUBE_VERSION_PATH), e);
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.application.config;
-
-import javax.annotation.ParametersAreNonnullByDefault;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.application;
-
-import javax.annotation.ParametersAreNonnullByDefault;
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.io.Closeable;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.concurrent.TimeUnit;
-import javax.annotation.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.ProcessId;
-
-abstract class AbstractProcessMonitor implements ProcessMonitor {
-
- private static final Logger LOG = LoggerFactory.getLogger(AbstractProcessMonitor.class);
- private static final int EXPECTED_EXIT_VALUE = 0;
-
- protected final Process process;
- private final ProcessId processId;
-
- protected AbstractProcessMonitor(Process process, ProcessId processId) {
- this.process = process;
- this.processId = processId;
- }
-
- public InputStream getInputStream() {
- return process.getInputStream();
- }
-
- public InputStream getErrorStream() {
- return process.getErrorStream();
- }
-
- public void closeStreams() {
- closeQuietly(process.getInputStream());
- closeQuietly(process.getOutputStream());
- closeQuietly(process.getErrorStream());
- }
-
- private static void closeQuietly(@Nullable Closeable closeable) {
- try {
- if (closeable != null) {
- closeable.close();
- }
- } catch (IOException ignored) {
- // ignore
- }
- }
-
- public boolean isAlive() {
- return process.isAlive();
- }
-
- public void destroyForcibly() {
- process.destroyForcibly();
- }
-
- public void waitFor() throws InterruptedException {
- int exitValue = process.waitFor();
- if (exitValue != EXPECTED_EXIT_VALUE) {
- LOG.warn("Process exited with exit value [{}]: {}", processId.getKey(), exitValue);
- } else {
- LOG.debug("Process exited with exit value [{}]: {}", processId.getKey(), exitValue);
- }
- }
-
- public void waitFor(long timeout, TimeUnit unit) throws InterruptedException {
- process.waitFor(timeout, unit);
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import com.google.common.net.HostAndPort;
-import io.netty.util.ThreadDeathWatcher;
-import io.netty.util.concurrent.GlobalEventExecutor;
-import java.net.InetAddress;
-import java.net.UnknownHostException;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.concurrent.atomic.AtomicReference;
-import java.util.stream.Collectors;
-import org.elasticsearch.action.admin.cluster.health.ClusterHealthRequest;
-import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
-import org.elasticsearch.client.transport.NoNodeAvailableException;
-import org.elasticsearch.client.transport.TransportClient;
-import org.elasticsearch.cluster.health.ClusterHealthStatus;
-import org.elasticsearch.common.network.NetworkModule;
-import org.elasticsearch.common.settings.Settings;
-import org.elasticsearch.common.transport.InetSocketTransportAddress;
-import org.elasticsearch.common.transport.TransportAddress;
-import org.elasticsearch.transport.Netty4Plugin;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.ProcessId;
-import org.sonar.process.command.EsCommand;
-
-import static java.util.Collections.singletonList;
-import static java.util.Collections.unmodifiableList;
-import static org.elasticsearch.common.unit.TimeValue.timeValueSeconds;
-import static org.sonar.application.process.EsProcessMonitor.Status.CONNECTION_REFUSED;
-import static org.sonar.application.process.EsProcessMonitor.Status.GREEN;
-import static org.sonar.application.process.EsProcessMonitor.Status.KO;
-import static org.sonar.application.process.EsProcessMonitor.Status.RED;
-import static org.sonar.application.process.EsProcessMonitor.Status.YELLOW;
-
-public class EsProcessMonitor extends AbstractProcessMonitor {
- private static final Logger LOG = LoggerFactory.getLogger(EsProcessMonitor.class);
- private static final int WAIT_FOR_UP_DELAY_IN_MILLIS = 100;
- private static final int WAIT_FOR_UP_TIMEOUT = 10 * 60; /* 1min */
-
- private final AtomicBoolean nodeUp = new AtomicBoolean(false);
- private final AtomicBoolean nodeOperational = new AtomicBoolean(false);
- private final EsCommand esCommand;
- private AtomicReference<TransportClient> transportClient = new AtomicReference<>(null);
-
- public EsProcessMonitor(Process process, ProcessId processId, EsCommand esCommand) {
- super(process, processId);
- this.esCommand = esCommand;
- }
-
- @Override
- public boolean isOperational() {
- if (nodeOperational.get()) {
- return true;
- }
-
- boolean flag = false;
- try {
- flag = checkOperational();
- } catch (InterruptedException e) {
- LOG.trace("Interrupted while checking ES node is operational", e);
- Thread.currentThread().interrupt();
- } finally {
- if (flag) {
- transportClient.set(null);
- nodeOperational.set(true);
- }
- }
- return nodeOperational.get();
- }
-
- private boolean checkOperational() throws InterruptedException {
- int i = 0;
- Status status = checkStatus();
- do {
- if (status != Status.CONNECTION_REFUSED) {
- nodeUp.set(true);
- } else {
- Thread.sleep(WAIT_FOR_UP_DELAY_IN_MILLIS);
- i++;
- status = checkStatus();
- }
- } while (!nodeUp.get() && i < WAIT_FOR_UP_TIMEOUT);
- return status == YELLOW || status == GREEN;
- }
-
- static class MinimalTransportClient extends TransportClient {
-
- MinimalTransportClient(Settings settings) {
- super(settings, unmodifiableList(singletonList(Netty4Plugin.class)));
- }
-
- @Override
- public void close() {
- super.close();
- if (NetworkModule.TRANSPORT_TYPE_SETTING.exists(settings) == false
- || NetworkModule.TRANSPORT_TYPE_SETTING.get(settings).equals(Netty4Plugin.NETTY_TRANSPORT_NAME)) {
- try {
- GlobalEventExecutor.INSTANCE.awaitInactivity(5, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- try {
- ThreadDeathWatcher.awaitInactivity(5, TimeUnit.SECONDS);
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- }
- }
- }
-
- }
-
- private Status checkStatus() {
- try {
- ClusterHealthResponse response = getTransportClient().admin().cluster()
- .health(new ClusterHealthRequest().waitForStatus(ClusterHealthStatus.YELLOW).timeout(timeValueSeconds(30)))
- .actionGet();
- if (response.getStatus() == ClusterHealthStatus.GREEN) {
- return GREEN;
- }
- if (response.getStatus() == ClusterHealthStatus.YELLOW) {
- return YELLOW;
- }
- if (response.getStatus() == ClusterHealthStatus.RED) {
- return RED;
- }
- return KO;
- } catch (NoNodeAvailableException e) {
- return CONNECTION_REFUSED;
- } catch (Exception e) {
- LOG.error("Failed to check status", e);
- return KO;
- }
- }
-
- private TransportClient getTransportClient() {
- TransportClient res = this.transportClient.get();
- if (res == null) {
- res = buildTransportClient();
- if (this.transportClient.compareAndSet(null, res)) {
- return res;
- }
- return this.transportClient.get();
- }
- return res;
- }
-
- private TransportClient buildTransportClient() {
- Settings.Builder esSettings = Settings.builder();
-
- // mandatory property defined by bootstrap process
- esSettings.put("cluster.name", esCommand.getClusterName());
-
- TransportClient nativeClient = new MinimalTransportClient(esSettings.build());
- HostAndPort host = HostAndPort.fromParts(esCommand.getHost(), esCommand.getPort());
- addHostToClient(host, nativeClient);
- if (LOG.isDebugEnabled()) {
- LOG.debug("Connected to Elasticsearch node: [{}]", displayedAddresses(nativeClient));
- }
- return nativeClient;
- }
-
- private static void addHostToClient(HostAndPort host, TransportClient client) {
- try {
- client.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(host.getHostText()), host.getPortOrDefault(9001)));
- } catch (UnknownHostException e) {
- throw new IllegalStateException("Can not resolve host [" + host + "]", e);
- }
- }
-
- private static String displayedAddresses(TransportClient nativeClient) {
- return nativeClient.transportAddresses().stream().map(TransportAddress::toString).collect(Collectors.joining(", "));
- }
-
- enum Status {
- CONNECTION_REFUSED, KO, RED, YELLOW, GREEN
- }
-
- @Override
- public void askForStop() {
- process.destroy();
- }
-
- @Override
- public boolean askedForRestart() {
- // ES does not support asking for restart
- return false;
- }
-
- @Override
- public void acknowledgeAskForRestart() {
- // nothing to do
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.ProcessId;
-
-import static org.sonar.application.process.Lifecycle.State.INIT;
-import static org.sonar.application.process.Lifecycle.State.STARTED;
-import static org.sonar.application.process.Lifecycle.State.STARTING;
-import static org.sonar.application.process.Lifecycle.State.STOPPED;
-import static org.sonar.application.process.Lifecycle.State.STOPPING;
-
-public class Lifecycle {
-
- public enum State {
- INIT, STARTING, STARTED, STOPPING, STOPPED
- }
-
- private static final Logger LOG = LoggerFactory.getLogger(Lifecycle.class);
- private static final Map<State, Set<State>> TRANSITIONS = buildTransitions();
-
- private final ProcessId processId;
- private final List<ProcessLifecycleListener> listeners;
- private State state;
-
- public Lifecycle(ProcessId processId, List<ProcessLifecycleListener> listeners) {
- this(processId, listeners, INIT);
- }
-
- Lifecycle(ProcessId processId, List<ProcessLifecycleListener> listeners, State initialState) {
- this.processId = processId;
- this.listeners = listeners;
- this.state = initialState;
- }
-
- private static Map<State, Set<State>> buildTransitions() {
- Map<State, Set<State>> res = new EnumMap<>(State.class);
- res.put(INIT, toSet(STARTING));
- res.put(STARTING, toSet(STARTED, STOPPING, STOPPED));
- res.put(STARTED, toSet(STOPPING, STOPPED));
- res.put(STOPPING, toSet(STOPPED));
- res.put(STOPPED, toSet());
- return res;
- }
-
- private static Set<State> toSet(State... states) {
- if (states.length == 0) {
- return Collections.emptySet();
- }
- if (states.length == 1) {
- return Collections.singleton(states[0]);
- }
- return EnumSet.copyOf(Arrays.asList(states));
- }
-
- State getState() {
- return state;
- }
-
- synchronized boolean tryToMoveTo(State to) {
- boolean res = false;
- State currentState = state;
- if (TRANSITIONS.get(currentState).contains(to)) {
- this.state = to;
- res = true;
- listeners.forEach(listener -> listener.onProcessState(processId, to));
- }
- LOG.trace("tryToMoveTo {} from {} to {} => {}", processId.getKey(), currentState, to, res);
- return res;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import org.sonar.process.ProcessId;
-import org.sonar.process.sharedmemoryfile.ProcessCommands;
-
-import static java.util.Objects.requireNonNull;
-
-class ProcessCommandsProcessMonitor extends AbstractProcessMonitor {
-
- private final ProcessCommands commands;
-
- ProcessCommandsProcessMonitor(Process process, ProcessId processId, ProcessCommands commands) {
- super(process, processId);
- this.commands = requireNonNull(commands, "commands can't be null");
- }
-
- /**
- * Whether the process has set the operational flag (in ipc shared memory)
- */
- @Override
- public boolean isOperational() {
- return commands.isOperational();
- }
-
- /**
- * Send request to gracefully stop to the process (via ipc shared memory)
- */
- @Override
- public void askForStop() {
- commands.askForStop();
- }
-
- /**
- * Whether the process asked for a full restart (via ipc shared memory)
- */
- @Override
- public boolean askedForRestart() {
- return commands.askedForRestart();
- }
-
- /**
- * Removes the flag in ipc shared memory so that next call to {@link #askedForRestart()}
- * returns {@code false}, except if meanwhile process asks again for restart.
- */
- @Override
- public void acknowledgeAskForRestart() {
- commands.acknowledgeAskForRestart();
- }
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import org.sonar.process.ProcessId;
-
-@FunctionalInterface
-public interface ProcessEventListener {
-
- enum Type {
- OPERATIONAL,
- ASK_FOR_RESTART
- }
-
- /**
- * This method is called when the process with the specified {@link ProcessId}
- * sends the event through the ipc shared memory.
- * Note that there can be a delay since the instant the process sets the flag
- * (see {@link SQProcess#WATCHER_DELAY_MS}).
- *
- * Call blocks the process watcher. Implementations should be asynchronous and
- * fork a new thread if call can be long.
- */
- void onProcessEvent(ProcessId processId, Type type);
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.io.Closeable;
-import org.sonar.process.command.EsCommand;
-import org.sonar.process.command.JavaCommand;
-
-public interface ProcessLauncher extends Closeable {
-
- @Override
- void close();
-
- /**
- * Launch an ES command.
- *
- * @throws IllegalStateException if an error occurs
- */
- ProcessMonitor launch(EsCommand esCommand);
-
- /**
- * Launch a Java command.
- *
- * @throws IllegalStateException if an error occurs
- */
- ProcessMonitor launch(JavaCommand javaCommand);
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.function.Supplier;
-import org.apache.commons.io.IOUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.ProcessId;
-import org.sonar.process.command.AbstractCommand;
-import org.sonar.process.command.EsCommand;
-import org.sonar.process.command.JavaCommand;
-import org.sonar.process.jmvoptions.JvmOptions;
-import org.sonar.process.sharedmemoryfile.AllProcessesCommands;
-import org.sonar.process.sharedmemoryfile.ProcessCommands;
-
-import static java.lang.String.format;
-import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_INDEX;
-import static org.sonar.process.ProcessEntryPoint.PROPERTY_PROCESS_KEY;
-import static org.sonar.process.ProcessEntryPoint.PROPERTY_SHARED_PATH;
-import static org.sonar.process.ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT;
-
-public class ProcessLauncherImpl implements ProcessLauncher {
- private static final Logger LOG = LoggerFactory.getLogger(ProcessLauncherImpl.class);
-
- private final File tempDir;
- private final AllProcessesCommands allProcessesCommands;
- private final Supplier<ProcessBuilder> processBuilderSupplier;
-
- public ProcessLauncherImpl(File tempDir) {
- this(tempDir, new AllProcessesCommands(tempDir), JavaLangProcessBuilder::new);
- }
-
- ProcessLauncherImpl(File tempDir, AllProcessesCommands allProcessesCommands, Supplier<ProcessBuilder> processBuilderSupplier) {
- this.tempDir = tempDir;
- this.allProcessesCommands = allProcessesCommands;
- this.processBuilderSupplier = processBuilderSupplier;
- }
-
- @Override
- public void close() {
- allProcessesCommands.close();
- }
-
- @Override
- public ProcessMonitor launch(EsCommand esCommand) {
- Process process = null;
- ProcessId processId = esCommand.getProcessId();
- try {
- writeConfFiles(esCommand);
- ProcessBuilder processBuilder = create(esCommand);
- logLaunchedCommand(esCommand, processBuilder);
-
- process = processBuilder.start();
-
- return new EsProcessMonitor(process, processId, esCommand);
- } catch (Exception e) {
- // just in case
- if (process != null) {
- process.destroyForcibly();
- }
- throw new IllegalStateException(format("Fail to launch process [%s]", processId.getKey()), e);
- }
- }
-
- private void writeConfFiles(EsCommand esCommand) {
- File confDir = esCommand.getConfDir();
- if (!confDir.exists() && !confDir.mkdirs()) {
- String error = format("Failed to create temporary configuration directory [%s]", confDir.getAbsolutePath());
- LOG.error(error);
- throw new IllegalStateException(error);
- }
-
- try {
- IOUtils.copy(getClass().getResourceAsStream("elasticsearch.yml"), new FileOutputStream(new File(confDir, "elasticsearch.yml")));
- esCommand.getEsJvmOptions().writeToJvmOptionFile(new File(confDir, "jvm.options"));
- esCommand.getLog4j2Properties().store(new FileOutputStream(new File(confDir, "log4j2.properties")), "log42 properties file for ES bundled in SonarQube");
- } catch (IOException e) {
- throw new IllegalStateException("Failed to write ES configuration files", e);
- }
- }
-
- @Override
- public ProcessMonitor launch(JavaCommand javaCommand) {
- Process process = null;
- ProcessId processId = javaCommand.getProcessId();
- try {
- ProcessCommands commands = allProcessesCommands.createAfterClean(processId.getIpcIndex());
-
- ProcessBuilder processBuilder = create(javaCommand);
- logLaunchedCommand(javaCommand, processBuilder);
- process = processBuilder.start();
- return new ProcessCommandsProcessMonitor(process, processId, commands);
- } catch (Exception e) {
- // just in case
- if (process != null) {
- process.destroyForcibly();
- }
- throw new IllegalStateException(format("Fail to launch process [%s]", processId.getKey()), e);
- }
- }
-
- private static <T extends AbstractCommand> void logLaunchedCommand(AbstractCommand<T> command, ProcessBuilder processBuilder) {
- if (LOG.isInfoEnabled()) {
- LOG.info("Launch process[{}] from [{}]: {}",
- command.getProcessId(),
- command.getWorkDir().getAbsolutePath(),
- String.join(" ", processBuilder.command()));
- }
- }
-
- private ProcessBuilder create(EsCommand esCommand) {
- List<String> commands = new ArrayList<>();
- commands.add(esCommand.getExecutable().getAbsolutePath());
- commands.addAll(esCommand.getEsOptions());
-
- return create(esCommand, commands);
- }
-
- private <T extends JvmOptions> ProcessBuilder create(JavaCommand<T> javaCommand) {
- List<String> commands = new ArrayList<>();
- commands.add(buildJavaPath());
- commands.addAll(javaCommand.getJvmOptions().getAll());
- commands.addAll(buildClasspath(javaCommand));
- commands.add(javaCommand.getClassName());
- commands.add(buildPropertiesFile(javaCommand).getAbsolutePath());
-
- return create(javaCommand, commands);
- }
-
- private ProcessBuilder create(AbstractCommand<?> javaCommand, List<String> commands) {
- ProcessBuilder processBuilder = processBuilderSupplier.get();
- processBuilder.command(commands);
- processBuilder.directory(javaCommand.getWorkDir());
- processBuilder.environment().putAll(javaCommand.getEnvVariables());
- processBuilder.redirectErrorStream(true);
- return processBuilder;
- }
-
- private static String buildJavaPath() {
- String separator = System.getProperty("file.separator");
- return new File(new File(System.getProperty("java.home")), "bin" + separator + "java").getAbsolutePath();
- }
-
- private static List<String> buildClasspath(JavaCommand javaCommand) {
- String pathSeparator = System.getProperty("path.separator");
- return Arrays.asList("-cp", String.join(pathSeparator, javaCommand.getClasspath()));
- }
-
- private File buildPropertiesFile(JavaCommand javaCommand) {
- File propertiesFile = null;
- try {
- propertiesFile = File.createTempFile("sq-process", "properties", tempDir);
- Properties props = new Properties();
- props.putAll(javaCommand.getArguments());
- props.setProperty(PROPERTY_PROCESS_KEY, javaCommand.getProcessId().getKey());
- props.setProperty(PROPERTY_PROCESS_INDEX, Integer.toString(javaCommand.getProcessId().getIpcIndex()));
- props.setProperty(PROPERTY_TERMINATION_TIMEOUT, "60000");
- props.setProperty(PROPERTY_SHARED_PATH, tempDir.getAbsolutePath());
- try (OutputStream out = new FileOutputStream(propertiesFile)) {
- props.store(out, format("Temporary properties file for command [%s]", javaCommand.getProcessId().getKey()));
- }
- return propertiesFile;
- } catch (Exception e) {
- throw new IllegalStateException("Cannot write temporary settings to " + propertiesFile, e);
- }
- }
-
- /**
- * An interface of the methods of {@link java.lang.ProcessBuilder} that we use in {@link ProcessLauncherImpl}.
- * <p>Allows testing creating processes without actualling creating them at OS level</p>
- */
- public interface ProcessBuilder {
- List<String> command();
-
- ProcessBuilder command(List<String> commands);
-
- ProcessBuilder directory(File dir);
-
- Map<String, String> environment();
-
- ProcessBuilder redirectErrorStream(boolean b);
-
- Process start() throws IOException;
- }
-
- private static class JavaLangProcessBuilder implements ProcessBuilder {
- private final java.lang.ProcessBuilder builder = new java.lang.ProcessBuilder();
-
- /**
- * @see java.lang.ProcessBuilder#command()
- */
- @Override
- public List<String> command() {
- return builder.command();
- }
-
- /**
- * @see java.lang.ProcessBuilder#command(List)
- */
- @Override
- public ProcessBuilder command(List<String> commands) {
- builder.command(commands);
- return this;
- }
-
- /**
- * @see java.lang.ProcessBuilder#directory(File)
- */
- @Override
- public ProcessBuilder directory(File dir) {
- builder.directory(dir);
- return this;
- }
-
- /**
- * @see java.lang.ProcessBuilder#environment()
- */
- @Override
- public Map<String, String> environment() {
- return builder.environment();
- }
-
- /**
- * @see java.lang.ProcessBuilder#redirectErrorStream(boolean)
- */
- @Override
- public ProcessBuilder redirectErrorStream(boolean b) {
- builder.redirectErrorStream(b);
- return this;
- }
-
- /**
- * @see java.lang.ProcessBuilder#start()
- */
- @Override
- public Process start() throws IOException {
- return builder.start();
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import org.sonar.process.ProcessId;
-
-@FunctionalInterface
-public interface ProcessLifecycleListener {
-
- /**
- * This method is called when the state of the process with the specified {@link ProcessId}
- * changes.
- *
- * Call blocks the process watcher. Implementations should be asynchronous and
- * fork a new thread if call can be long.
- */
- void onProcessState(ProcessId processId, Lifecycle.State state);
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.io.InputStream;
-import java.util.concurrent.TimeUnit;
-
-public interface ProcessMonitor {
-
- /**
- * @see Process#getInputStream()
- */
- InputStream getInputStream();
-
- /**
- * @see Process#getErrorStream()
- */
- InputStream getErrorStream();
-
- /**
- * Closes the streams {@link Process#getInputStream()}, {@link Process#getOutputStream()}
- * and {@link Process#getErrorStream()}.
- *
- * No exceptions are thrown in case of errors.
- */
- void closeStreams();
-
- /**
- * @see Process#isAlive()
- */
- boolean isAlive();
-
- /**
- * @see Process#destroyForcibly()
- */
- void destroyForcibly();
-
- /**
- * @see Process#waitFor()
- */
- void waitFor() throws InterruptedException;
-
- /**
- * @see Process#waitFor(long, TimeUnit)
- */
- void waitFor(long timeout, TimeUnit timeoutUnit) throws InterruptedException;
-
- /**
- * Whether the process has reach operational state after startup.
- */
- boolean isOperational();
-
- /**
- * Send request to gracefully stop to the process
- */
- void askForStop();
-
- /**
- * Whether the process asked for a full restart
- */
- boolean askedForRestart();
-
- /**
- * Sends a signal to the process to acknowledge that the parent process received the request to restart from the
- * child process send via {@link #askedForRestart()}.
- * <br/>
- * Child process will typically stop sending the signal requesting restart from now on.
- */
- void acknowledgeAskForRestart();
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.process;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicBoolean;
-import java.util.function.Supplier;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.ProcessId;
-
-import static java.lang.String.format;
-import static java.util.Objects.requireNonNull;
-
-public class SQProcess {
-
- public static final long DEFAULT_WATCHER_DELAY_MS = 500L;
- private static final Logger LOG = LoggerFactory.getLogger(SQProcess.class);
-
- private final ProcessId processId;
- private final Lifecycle lifecycle;
- private final List<ProcessEventListener> eventListeners;
- private final long watcherDelayMs;
-
- private ProcessMonitor process;
- private StreamGobbler stdOutGobbler;
- private StreamGobbler stdErrGobbler;
- private final StopWatcher stopWatcher;
- private final EventWatcher eventWatcher;
- // keep flag so that the operational event is sent only once
- // to listeners
- private final AtomicBoolean operational = new AtomicBoolean(false);
-
- private SQProcess(Builder builder) {
- this.processId = requireNonNull(builder.processId, "processId can't be null");
- this.lifecycle = new Lifecycle(this.processId, builder.lifecycleListeners);
- this.eventListeners = builder.eventListeners;
- this.watcherDelayMs = builder.watcherDelayMs;
- this.stopWatcher = new StopWatcher();
- this.eventWatcher = new EventWatcher();
- }
-
- public boolean start(Supplier<ProcessMonitor> commandLauncher) {
- if (!lifecycle.tryToMoveTo(Lifecycle.State.STARTING)) {
- // has already been started
- return false;
- }
- try {
- this.process = commandLauncher.get();
- } catch (RuntimeException e) {
- LOG.error(format("Fail to launch process [%s]", processId.getKey()), e);
- lifecycle.tryToMoveTo(Lifecycle.State.STOPPED);
- throw e;
- }
- this.stdOutGobbler = new StreamGobbler(process.getInputStream(), processId.getKey());
- this.stdOutGobbler.start();
- this.stdErrGobbler = new StreamGobbler(process.getErrorStream(), processId.getKey());
- this.stdErrGobbler.start();
- this.stopWatcher.start();
- this.eventWatcher.start();
- // Could be improved by checking the status "up" in shared memory.
- // Not a problem so far as this state is not used by listeners.
- lifecycle.tryToMoveTo(Lifecycle.State.STARTED);
- return true;
- }
-
- public ProcessId getProcessId() {
- return processId;
- }
-
- Lifecycle.State getState() {
- return lifecycle.getState();
- }
-
- /**
- * Sends kill signal and awaits termination. No guarantee that process is gracefully terminated (=shutdown hooks
- * executed). It depends on OS.
- */
- public void stop(long timeout, TimeUnit timeoutUnit) {
- if (lifecycle.tryToMoveTo(Lifecycle.State.STOPPING)) {
- stopGracefully(timeout, timeoutUnit);
- if (process != null && process.isAlive()) {
- LOG.info("{} failed to stop in a timely fashion. Killing it.", processId.getKey());
- }
- // enforce stop and clean-up even if process has been gracefully stopped
- stopForcibly();
- } else {
- // already stopping or stopped
- waitForDown();
- }
- }
-
- private void waitForDown() {
- while (process != null && process.isAlive()) {
- try {
- process.waitFor();
- } catch (InterruptedException ignored) {
- // ignore, waiting for process to stop
- Thread.currentThread().interrupt();
- }
- }
- }
-
- private void stopGracefully(long timeout, TimeUnit timeoutUnit) {
- if (process == null) {
- return;
- }
- try {
- // request graceful stop
- process.askForStop();
- process.waitFor(timeout, timeoutUnit);
- } catch (InterruptedException e) {
- // can't wait for the termination of process. Let's assume it's down.
- LOG.warn(format("Interrupted while stopping process %s", processId), e);
- Thread.currentThread().interrupt();
- } catch (Throwable e) {
- LOG.error("Can not ask for graceful stop of process " + processId, e);
- }
- }
-
- public void stopForcibly() {
- eventWatcher.interrupt();
- stopWatcher.interrupt();
- if (process != null) {
- process.destroyForcibly();
- waitForDown();
- process.closeStreams();
- }
- if (stdOutGobbler != null) {
- StreamGobbler.waitUntilFinish(stdOutGobbler);
- stdOutGobbler.interrupt();
- }
- if (stdErrGobbler != null) {
- StreamGobbler.waitUntilFinish(stdErrGobbler);
- stdErrGobbler.interrupt();
- }
- lifecycle.tryToMoveTo(Lifecycle.State.STOPPED);
- }
-
- void refreshState() {
- if (process.isAlive()) {
- if (!operational.get() && process.isOperational()) {
- operational.set(true);
- eventListeners.forEach(l -> l.onProcessEvent(processId, ProcessEventListener.Type.OPERATIONAL));
- }
- if (process.askedForRestart()) {
- process.acknowledgeAskForRestart();
- eventListeners.forEach(l -> l.onProcessEvent(processId, ProcessEventListener.Type.ASK_FOR_RESTART));
- }
- } else {
- stopForcibly();
- }
- }
-
- @Override
- public String toString() {
- return format("Process[%s]", processId.getKey());
- }
-
- /**
- * This thread blocks as long as the monitored process is physically alive.
- * It avoids from executing {@link Process#exitValue()} at a fixed rate :
- * <ul>
- * <li>no usage of exception for flow control. Indeed {@link Process#exitValue()} throws an exception
- * if process is alive. There's no method <code>Process#isAlive()</code></li>
- * <li>no delay, instantaneous notification that process is down</li>
- * </ul>
- */
- private class StopWatcher extends Thread {
- StopWatcher() {
- // this name is different than Thread#toString(), which includes name, priority
- // and thread group
- // -> do not override toString()
- super(format("StopWatcher[%s]", processId.getKey()));
- }
-
- @Override
- public void run() {
- try {
- process.waitFor();
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- // stop watching process
- }
- stopForcibly();
- }
- }
-
- private class EventWatcher extends Thread {
- EventWatcher() {
- // this name is different than Thread#toString(), which includes name, priority
- // and thread group
- // -> do not override toString()
- super(format("EventWatcher[%s]", processId.getKey()));
- }
-
- @Override
- public void run() {
- try {
- while (process.isAlive()) {
- refreshState();
- Thread.sleep(watcherDelayMs);
- }
- } catch (InterruptedException e) {
- // request to stop watching process. To avoid unexpected behaviors
- // the process is stopped.
- Thread.currentThread().interrupt();
- stopForcibly();
- }
- }
- }
-
- public static Builder builder(ProcessId processId) {
- return new Builder(processId);
- }
-
- public static class Builder {
- private final ProcessId processId;
- private final List<ProcessEventListener> eventListeners = new ArrayList<>();
- private final List<ProcessLifecycleListener> lifecycleListeners = new ArrayList<>();
- private long watcherDelayMs = DEFAULT_WATCHER_DELAY_MS;
-
- private Builder(ProcessId processId) {
- this.processId = processId;
- }
-
- public Builder addEventListener(ProcessEventListener listener) {
- this.eventListeners.add(listener);
- return this;
- }
-
- public Builder addProcessLifecycleListener(ProcessLifecycleListener listener) {
- this.lifecycleListeners.add(listener);
- return this;
- }
-
- /**
- * Default delay is {@link #DEFAULT_WATCHER_DELAY_MS}
- */
- public Builder setWatcherDelayMs(long l) {
- this.watcherDelayMs = l;
- return this;
- }
-
- public SQProcess build() {
- return new SQProcess(this);
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-/**
- * Background thread that checks if a stop request
- * is sent, usually by Orchestrator
- */
-public interface StopRequestWatcher {
-
- void startWatching();
-
- void stopWatching();
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import org.sonar.application.FileSystem;
-import org.sonar.application.Scheduler;
-import org.sonar.application.config.AppSettings;
-import org.sonar.process.sharedmemoryfile.DefaultProcessCommands;
-import org.sonar.process.sharedmemoryfile.ProcessCommands;
-import org.sonar.process.ProcessId;
-import org.sonar.process.ProcessProperties;
-
-public class StopRequestWatcherImpl extends Thread implements StopRequestWatcher {
-
- private static final long DEFAULT_WATCHER_DELAY_MS = 500L;
-
- private final ProcessCommands commands;
- private final Scheduler scheduler;
- private final AppSettings settings;
- private long delayMs = DEFAULT_WATCHER_DELAY_MS;
-
- StopRequestWatcherImpl(AppSettings settings, Scheduler scheduler, ProcessCommands commands) {
- super("StopRequestWatcherImpl");
- this.settings = settings;
- this.commands = commands;
- this.scheduler = scheduler;
-
- // safeguard, do not block the JVM if thread is not interrupted
- // (method stopWatching() never called).
- setDaemon(true);
- }
-
- public static StopRequestWatcherImpl create(AppSettings settings, Scheduler scheduler, FileSystem fs) {
- DefaultProcessCommands commands = DefaultProcessCommands.secondary(fs.getTempDir(), ProcessId.APP.getIpcIndex());
- return new StopRequestWatcherImpl(settings, scheduler, commands);
- }
-
- long getDelayMs() {
- return delayMs;
- }
-
- void setDelayMs(long delayMs) {
- this.delayMs = delayMs;
- }
-
- @Override
- public void run() {
- try {
- while (true) {
- if (commands.askedForStop()) {
- scheduler.terminate();
- return;
- }
- Thread.sleep(delayMs);
- }
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- // stop watching the commands
- }
- }
-
- @Override
- public void startWatching() {
- if (settings.getProps().valueAsBoolean(ProcessProperties.ENABLE_STOP_COMMAND)) {
- start();
- }
- }
-
- @Override
- public void stopWatching() {
- // does nothing is not started
- interrupt();
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.io.BufferedReader;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import javax.annotation.Nullable;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import static java.nio.charset.StandardCharsets.UTF_8;
-
-/**
- * Reads process output and writes to logs
- */
-public class StreamGobbler extends Thread {
-
- public static final String LOGGER_GOBBLER = "gobbler";
-
- private final InputStream is;
- private final Logger logger;
-
- StreamGobbler(InputStream is, String processKey) {
- this(is, processKey, LoggerFactory.getLogger(LOGGER_GOBBLER));
- }
-
- StreamGobbler(InputStream is, String processKey, Logger logger) {
- super(String.format("Gobbler[%s]", processKey));
- this.is = is;
- this.logger = logger;
- }
-
- @Override
- public void run() {
- try (BufferedReader br = new BufferedReader(new InputStreamReader(is, UTF_8))) {
- String line;
- while ((line = br.readLine()) != null) {
- logger.info(line);
- }
- } catch (Exception ignored) {
- // ignored
- }
- }
-
- static void waitUntilFinish(@Nullable StreamGobbler gobbler) {
- if (gobbler != null) {
- try {
- gobbler.join();
- } catch (InterruptedException ignored) {
- // consider as finished, restore the interrupted flag
- Thread.currentThread().interrupt();
- }
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-@ParametersAreNonnullByDefault
-package org.sonar.application.process;
-
-import javax.annotation.ParametersAreNonnullByDefault;
+++ /dev/null
-# ======================== Elasticsearch Configuration =========================
-#
-# NOTE: Elasticsearch comes with reasonable defaults for most settings.
-# Before you set out to tweak and tune the configuration, make sure you
-# understand what are you trying to accomplish and the consequences.
-#
-# The primary way of configuring a node is via this file. This template lists
-# the most important settings you may want to configure for a production cluster.
-#
-# Please see the documentation for further information on configuration options:
-# <https://www.elastic.co/guide/en/elasticsearch/reference/5.0/settings.html>
-#
-# ---------------------------------- Cluster -----------------------------------
-#
-# Use a descriptive name for your cluster:
-#
-#cluster.name: my-application
-#
-# ------------------------------------ Node ------------------------------------
-#
-# Use a descriptive name for the node:
-#
-#node.name: node-1
-#
-# Add custom attributes to the node:
-#
-#node.attr.rack: r1
-#
-# ----------------------------------- Paths ------------------------------------
-#
-# Path to directory where to store the data (separate multiple locations by comma):
-#
-#path.data: /path/to/data
-#
-# Path to log files:
-#
-#path.logs: /path/to/logs
-#
-# ----------------------------------- Memory -----------------------------------
-#
-# Lock the memory on startup:
-#
-#bootstrap.memory_lock: true
-#
-# Make sure that the heap size is set to about half the memory available
-# on the system and that the owner of the process is allowed to use this
-# limit.
-#
-# Elasticsearch performs poorly when the system is swapping the memory.
-#
-# ---------------------------------- Network -----------------------------------
-#
-# Set the bind address to a specific IP (IPv4 or IPv6):
-#
-#network.host: 192.168.0.1
-#
-# Set a custom port for HTTP:
-#
-#http.port: 9200
-#
-# For more information, see the documentation at:
-# <https://www.elastic.co/guide/en/elasticsearch/reference/5.0/modules-network.html>
-#
-# --------------------------------- Discovery ----------------------------------
-#
-# Pass an initial list of hosts to perform discovery when new node is started:
-# The default list of hosts is ["127.0.0.1", "[::1]"]
-#
-#discovery.zen.ping.unicast.hosts: ["host1", "host2"]
-#
-# Prevent the "split brain" by configuring the majority of nodes (total number of nodes / 2 + 1):
-#
-#discovery.zen.minimum_master_nodes: 3
-#
-# For more information, see the documentation at:
-# <https://www.elastic.co/guide/en/elasticsearch/reference/5.0/modules-discovery-zen.html>
-#
-# ---------------------------------- Gateway -----------------------------------
-#
-# Block initial recovery after a full cluster restart until N nodes are started:
-#
-#gateway.recover_after_nodes: 3
-#
-# For more information, see the documentation at:
-# <https://www.elastic.co/guide/en/elasticsearch/reference/5.0/modules-gateway.html>
-#
-# ---------------------------------- Various -----------------------------------
-#
-# Require explicit names when deleting indices:
-#
-#action.destructive_requires_name: true
+++ /dev/null
-${buildVersion}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.io.File;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.attribute.BasicFileAttributes;
-import javax.annotation.CheckForNull;
-import org.apache.commons.io.FileUtils;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-import org.sonar.application.config.TestAppSettings;
-import org.sonar.process.sharedmemoryfile.AllProcessesCommands;
-import org.sonar.process.ProcessProperties;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.process.sharedmemoryfile.ProcessCommands.MAX_PROCESSES;
-
-public class AppFileSystemTest {
-
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- private File homeDir;
- private File dataDir;
- private File tempDir;
- private File logsDir;
- private File webDir;
- private TestAppSettings settings = new TestAppSettings();
- private AppFileSystem underTest = new AppFileSystem(settings);
-
- @Before
- public void before() throws IOException {
- homeDir = temp.newFolder();
- dataDir = new File(homeDir, "data");
- tempDir = new File(homeDir, "temp");
- logsDir = new File(homeDir, "logs");
- webDir = new File(homeDir, "web");
-
- settings.getProps().set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
- settings.getProps().set(ProcessProperties.PATH_DATA, dataDir.getAbsolutePath());
- settings.getProps().set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath());
- settings.getProps().set(ProcessProperties.PATH_LOGS, logsDir.getAbsolutePath());
- settings.getProps().set(ProcessProperties.PATH_WEB, webDir.getAbsolutePath());
- }
-
- @Test
- public void reset_creates_dirs_if_they_don_t_exist() throws Exception {
- assertThat(dataDir).doesNotExist();
-
- underTest.reset();
-
- assertThat(dataDir).exists().isDirectory();
- assertThat(logsDir).exists().isDirectory();
- assertThat(tempDir).exists().isDirectory();
- assertThat(webDir).exists().isDirectory();
-
- underTest.reset();
-
- assertThat(dataDir).exists().isDirectory();
- assertThat(logsDir).exists().isDirectory();
- assertThat(tempDir).exists().isDirectory();
- assertThat(webDir).exists().isDirectory();
- }
-
- @Test
- public void reset_deletes_content_of_temp_dir_but_not_temp_dir_itself_if_it_already_exists() throws Exception {
- assertThat(tempDir.mkdir()).isTrue();
- Object tempDirKey = getFileKey(tempDir);
- File fileInTempDir = new File(tempDir, "someFile.txt");
- assertThat(fileInTempDir.createNewFile()).isTrue();
- File subDirInTempDir = new File(tempDir, "subDir");
- assertThat(subDirInTempDir.mkdir()).isTrue();
-
- underTest.reset();
-
- assertThat(tempDir).exists();
- assertThat(fileInTempDir).doesNotExist();
- assertThat(subDirInTempDir).doesNotExist();
- assertThat(getFileKey(tempDir)).isEqualTo(tempDirKey);
- }
-
- @Test
- public void reset_deletes_content_of_temp_dir_but_not_sharedmemory_file() throws Exception {
- assertThat(tempDir.mkdir()).isTrue();
- File sharedmemory = new File(tempDir, "sharedmemory");
- assertThat(sharedmemory.createNewFile()).isTrue();
- FileUtils.write(sharedmemory, "toto");
- Object fileKey = getFileKey(sharedmemory);
-
- Object tempDirKey = getFileKey(tempDir);
- File fileInTempDir = new File(tempDir, "someFile.txt");
- assertThat(fileInTempDir.createNewFile()).isTrue();
-
- underTest.reset();
-
- assertThat(tempDir).exists();
- assertThat(fileInTempDir).doesNotExist();
- assertThat(getFileKey(tempDir)).isEqualTo(tempDirKey);
- assertThat(getFileKey(sharedmemory)).isEqualTo(fileKey);
- // content of sharedMemory file is reset
- assertThat(FileUtils.readFileToString(sharedmemory)).isNotEqualTo("toto");
- }
-
- @Test
- public void reset_cleans_the_sharedmemory_file() throws IOException {
- assertThat(tempDir.mkdir()).isTrue();
- try (AllProcessesCommands commands = new AllProcessesCommands(tempDir)) {
- for (int i = 0; i < MAX_PROCESSES; i++) {
- commands.create(i).setUp();
- }
-
- underTest.reset();
-
- for (int i = 0; i < MAX_PROCESSES; i++) {
- assertThat(commands.create(i).isUp()).isFalse();
- }
- }
- }
-
- @CheckForNull
- private static Object getFileKey(File fileInTempDir) throws IOException {
- Path path = Paths.get(fileInTempDir.toURI());
- BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class);
- return attrs.fileKey();
- }
-
- @Test
- public void reset_throws_ISE_if_data_dir_is_a_file() throws Exception {
- resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_DATA);
- }
-
- @Test
- public void reset_throws_ISE_if_web_dir_is_a_file() throws Exception {
- resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_WEB);
- }
-
- @Test
- public void reset_throws_ISE_if_logs_dir_is_a_file() throws Exception {
- resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_LOGS);
- }
-
- @Test
- public void reset_throws_ISE_if_temp_dir_is_a_file() throws Exception {
- resetThrowsISEIfDirIsAFile(ProcessProperties.PATH_TEMP);
- }
-
- private void resetThrowsISEIfDirIsAFile(String property) throws IOException {
- File file = new File(homeDir, "zoom.store");
- assertThat(file.createNewFile()).isTrue();
- settings.getProps().set(property, file.getAbsolutePath());
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Property '" + property + "' is not valid, not a directory: " + file.getAbsolutePath());
-
- underTest.reset();
- }
-
- @Test
- public void fail_if_required_directory_is_a_file() throws Exception {
- // <home>/data is missing
- FileUtils.forceMkdir(webDir);
- FileUtils.forceMkdir(logsDir);
- FileUtils.touch(dataDir);
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Property 'sonar.path.data' is not valid, not a directory: " + dataDir.getAbsolutePath());
-
- underTest.reset();
- }
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import ch.qos.logback.classic.Level;
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.Appender;
-import ch.qos.logback.core.ConsoleAppender;
-import ch.qos.logback.core.FileAppender;
-import ch.qos.logback.core.encoder.Encoder;
-import ch.qos.logback.core.joran.spi.ConsoleTarget;
-import ch.qos.logback.core.rolling.RollingFileAppender;
-import java.io.File;
-import java.io.IOException;
-import java.util.Iterator;
-import org.junit.AfterClass;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-import org.slf4j.LoggerFactory;
-import org.sonar.application.config.AppSettings;
-import org.sonar.application.config.TestAppSettings;
-import org.sonar.process.ProcessProperties;
-import org.sonar.process.logging.LogbackHelper;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.slf4j.Logger.ROOT_LOGGER_NAME;
-import static org.sonar.application.process.StreamGobbler.LOGGER_GOBBLER;
-
-public class AppLoggingTest {
-
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- private File logDir;
-
- private AppSettings settings = new TestAppSettings();
- private AppLogging underTest = new AppLogging(settings);
-
- @Before
- public void setUp() throws Exception {
- logDir = temp.newFolder();
- settings.getProps().set(ProcessProperties.PATH_LOGS, logDir.getAbsolutePath());
- }
-
- @AfterClass
- public static void resetLogback() throws Exception {
- new LogbackHelper().resetFromXml("/logback-test.xml");
- }
-
- @Test
- public void no_writing_to_sonar_log_file_when_running_from_sonar_script() {
- emulateRunFromSonarScript();
-
- LoggerContext ctx = underTest.configure();
-
- ctx.getLoggerList().forEach(AppLoggingTest::verifyNoFileAppender);
- }
-
- @Test
- public void root_logger_only_writes_to_console_with_formatting_when_running_from_sonar_script() {
- emulateRunFromSonarScript();
-
- LoggerContext ctx = underTest.configure();
-
- Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
- ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) rootLogger.getAppender("APP_CONSOLE");
- verifyAppFormattedLogEncoder(consoleAppender.getEncoder());
- assertThat(rootLogger.iteratorForAppenders()).hasSize(1);
- }
-
- @Test
- public void gobbler_logger_writes_to_console_without_formatting_when_running_from_sonar_script() {
- emulateRunFromSonarScript();
-
- LoggerContext ctx = underTest.configure();
-
- Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
- verifyGobblerConsoleAppender(gobblerLogger);
- assertThat(gobblerLogger.iteratorForAppenders()).hasSize(1);
- }
-
- @Test
- public void root_logger_writes_to_console_with_formatting_and_to_sonar_log_file_when_running_from_command_line() {
- emulateRunFromCommandLine(false);
-
- LoggerContext ctx = underTest.configure();
-
- Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
- verifyAppConsoleAppender(rootLogger.getAppender("APP_CONSOLE"));
- verifySonarLogFileAppender(rootLogger.getAppender("file_sonar"));
- assertThat(rootLogger.iteratorForAppenders()).hasSize(2);
-
- // verify no other logger writes to sonar.log
- ctx.getLoggerList()
- .stream()
- .filter(logger -> !ROOT_LOGGER_NAME.equals(logger.getName()))
- .forEach(AppLoggingTest::verifyNoFileAppender);
- }
-
- @Test
- public void gobbler_logger_writes_to_console_without_formatting_when_running_from_command_line() {
- emulateRunFromCommandLine(false);
-
- LoggerContext ctx = underTest.configure();
-
- Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
- verifyGobblerConsoleAppender(gobblerLogger);
- assertThat(gobblerLogger.iteratorForAppenders()).hasSize(1);
- }
-
- @Test
- public void root_logger_writes_to_console_with_formatting_and_to_sonar_log_file_when_running_from_ITs() {
- emulateRunFromCommandLine(true);
-
- LoggerContext ctx = underTest.configure();
-
- Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
- verifyAppConsoleAppender(rootLogger.getAppender("APP_CONSOLE"));
- verifySonarLogFileAppender(rootLogger.getAppender("file_sonar"));
- assertThat(rootLogger.iteratorForAppenders()).hasSize(2);
-
- ctx.getLoggerList()
- .stream()
- .filter(logger -> !ROOT_LOGGER_NAME.equals(logger.getName()))
- .forEach(AppLoggingTest::verifyNoFileAppender);
- }
-
- @Test
- public void gobbler_logger_writes_to_console_without_formatting_when_running_from_ITs() {
- emulateRunFromCommandLine(true);
-
- LoggerContext ctx = underTest.configure();
-
- Logger gobblerLogger = ctx.getLogger(LOGGER_GOBBLER);
- verifyGobblerConsoleAppender(gobblerLogger);
- assertThat(gobblerLogger.iteratorForAppenders()).hasSize(1);
- }
-
- @Test
- public void configure_no_rotation_on_sonar_file() {
- settings.getProps().set("sonar.log.rollingPolicy", "none");
-
- LoggerContext ctx = underTest.configure();
-
- Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
- Appender<ILoggingEvent> appender = rootLogger.getAppender("file_sonar");
- assertThat(appender)
- .isNotInstanceOf(RollingFileAppender.class)
- .isInstanceOf(FileAppender.class);
- }
-
- @Test
- public void default_level_for_root_logger_is_INFO() {
- LoggerContext ctx = underTest.configure();
-
- verifyRootLogLevel(ctx, Level.INFO);
- }
-
- @Test
- public void root_logger_level_changes_with_global_property() {
- settings.getProps().set("sonar.log.level", "TRACE");
-
- LoggerContext ctx = underTest.configure();
-
- verifyRootLogLevel(ctx, Level.TRACE);
- }
-
- @Test
- public void root_logger_level_changes_with_app_property() {
- settings.getProps().set("sonar.log.level.app", "TRACE");
-
- LoggerContext ctx = underTest.configure();
-
- verifyRootLogLevel(ctx, Level.TRACE);
- }
-
- @Test
- public void root_logger_level_is_configured_from_app_property_over_global_property() {
- settings.getProps().set("sonar.log.level", "TRACE");
- settings.getProps().set("sonar.log.level.app", "DEBUG");
-
- LoggerContext ctx = underTest.configure();
-
- verifyRootLogLevel(ctx, Level.DEBUG);
- }
-
- @Test
- public void root_logger_level_changes_with_app_property_and_is_case_insensitive() {
- settings.getProps().set("sonar.log.level.app", "debug");
-
- LoggerContext ctx = underTest.configure();
-
- verifyRootLogLevel(ctx, Level.DEBUG);
- }
-
- @Test
- public void default_to_INFO_if_app_property_has_invalid_value() {
- settings.getProps().set("sonar.log.level.app", "DodoDouh!");
-
- LoggerContext ctx = underTest.configure();
- verifyRootLogLevel(ctx, Level.INFO);
- }
-
- @Test
- public void fail_with_IAE_if_global_property_unsupported_level() {
- settings.getProps().set("sonar.log.level", "ERROR");
-
- expectedException.expect(IllegalArgumentException.class);
- expectedException.expectMessage("log level ERROR in property sonar.log.level is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
-
- underTest.configure();
- }
-
- @Test
- public void fail_with_IAE_if_app_property_unsupported_level() {
- settings.getProps().set("sonar.log.level.app", "ERROR");
-
- expectedException.expect(IllegalArgumentException.class);
- expectedException.expectMessage("log level ERROR in property sonar.log.level.app is not a supported value (allowed levels are [TRACE, DEBUG, INFO])");
-
- underTest.configure();
- }
-
- @Test
- public void no_info_log_from_hazelcast() throws IOException {
- settings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
- underTest.configure();
-
- assertThat(
- LoggerFactory.getLogger("com.hazelcast").isInfoEnabled()).isEqualTo(false);
- }
-
- @Test
- public void configure_logging_for_hazelcast() throws IOException {
- settings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
- settings.getProps().set(ProcessProperties.HAZELCAST_LOG_LEVEL, "INFO");
- underTest.configure();
-
- assertThat(
- LoggerFactory.getLogger("com.hazelcast").isInfoEnabled()).isEqualTo(true);
- assertThat(
- LoggerFactory.getLogger("com.hazelcast").isDebugEnabled()).isEqualTo(false);
- }
-
- private void emulateRunFromSonarScript() {
- settings.getProps().set("sonar.wrapped", "true");
- }
-
- private void emulateRunFromCommandLine(boolean withAllLogsPrintedToConsole) {
- if (withAllLogsPrintedToConsole) {
- settings.getProps().set("sonar.log.console", "true");
- }
- }
-
- private static void verifyNoFileAppender(Logger logger) {
- Iterator<Appender<ILoggingEvent>> iterator = logger.iteratorForAppenders();
- while (iterator.hasNext()) {
- assertThat(iterator.next()).isNotInstanceOf(FileAppender.class);
- }
- }
-
- private void verifySonarLogFileAppender(Appender<ILoggingEvent> appender) {
- assertThat(appender).isInstanceOf(FileAppender.class);
- FileAppender fileAppender = (FileAppender) appender;
- assertThat(fileAppender.getFile()).isEqualTo(new File(logDir, "sonar.log").getAbsolutePath());
- verifyAppFormattedLogEncoder(fileAppender.getEncoder());
- }
-
- private void verifyAppConsoleAppender(Appender<ILoggingEvent> appender) {
- assertThat(appender).isInstanceOf(ConsoleAppender.class);
- ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) appender;
- assertThat(consoleAppender.getTarget()).isEqualTo(ConsoleTarget.SystemOut.getName());
- verifyAppFormattedLogEncoder(consoleAppender.getEncoder());
- }
-
- private void verifyAppFormattedLogEncoder(Encoder<ILoggingEvent> encoder) {
- verifyFormattedLogEncoder(encoder, "%d{yyyy.MM.dd HH:mm:ss} %-5level app[][%logger{20}] %msg%n");
- }
-
- private void verifyGobblerConsoleAppender(Logger logger) {
- Appender<ILoggingEvent> appender = logger.getAppender("GOBBLER_CONSOLE");
- assertThat(appender).isInstanceOf(ConsoleAppender.class);
- ConsoleAppender<ILoggingEvent> consoleAppender = (ConsoleAppender<ILoggingEvent>) appender;
- assertThat(consoleAppender.getTarget()).isEqualTo(ConsoleTarget.SystemOut.getName());
- verifyFormattedLogEncoder(consoleAppender.getEncoder(), "%msg%n");
- }
-
- private void verifyFormattedLogEncoder(Encoder<ILoggingEvent> encoder, String logPattern) {
- assertThat(encoder).isInstanceOf(PatternLayoutEncoder.class);
- PatternLayoutEncoder patternEncoder = (PatternLayoutEncoder) encoder;
- assertThat(patternEncoder.getPattern()).isEqualTo(logPattern);
- }
-
- private void verifyRootLogLevel(LoggerContext ctx, Level expected) {
- Logger rootLogger = ctx.getLogger(ROOT_LOGGER_NAME);
- assertThat(rootLogger.getLevel()).isEqualTo(expected);
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.io.IOException;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.application.config.AppSettings;
-import org.sonar.application.config.AppSettingsLoader;
-import org.sonar.application.config.TestAppSettings;
-import org.sonar.process.MessageException;
-import org.sonar.process.ProcessProperties;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.data.MapEntry.entry;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-public class AppReloaderImplTest {
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- private AppSettingsLoader settingsLoader = mock(AppSettingsLoader.class);
- private FileSystem fs = mock(FileSystem.class);
- private AppState state = mock(AppState.class);
- private AppLogging logging = mock(AppLogging.class);
- private AppReloaderImpl underTest = new AppReloaderImpl(settingsLoader, fs, state, logging);
-
- @Test
- public void reload_configuration_then_reset_all() throws IOException {
- AppSettings settings = new TestAppSettings().set("foo", "bar");
- AppSettings newSettings = new TestAppSettings()
- .set("foo", "newBar")
- .set("newProp", "newVal");
- when(settingsLoader.load()).thenReturn(newSettings);
-
- underTest.reload(settings);
-
- assertThat(settings.getProps().rawProperties())
- .contains(entry("foo", "newBar"))
- .contains(entry("newProp", "newVal"));
- verify(logging).configure();
- verify(state).reset();
- verify(fs).reset();
- }
-
- @Test
- public void throw_ISE_if_cluster_is_enabled() throws IOException {
- AppSettings settings = new TestAppSettings().set(ProcessProperties.CLUSTER_ENABLED, "true");
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Restart is not possible with cluster mode");
-
- underTest.reload(settings);
-
- verifyZeroInteractions(logging);
- verifyZeroInteractions(state);
- verifyZeroInteractions(fs);
- }
-
- @Test
- public void throw_MessageException_if_path_properties_are_changed() throws IOException {
- verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_DATA);
- verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_LOGS);
- verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_TEMP);
- verifyFailureIfPropertyValueChanged(ProcessProperties.PATH_WEB);
- }
-
- @Test
- public void throw_MessageException_if_cluster_mode_changed() throws IOException {
- verifyFailureIfPropertyValueChanged(ProcessProperties.CLUSTER_ENABLED);
- }
-
- private void verifyFailureIfPropertyValueChanged(String propertyKey) throws IOException {
- AppSettings settings = new TestAppSettings().set(propertyKey, "val1");
- AppSettings newSettings = new TestAppSettings()
- .set(propertyKey, "val2");
- when(settingsLoader.load()).thenReturn(newSettings);
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Property [" + propertyKey + "] cannot be changed on restart: [val1] => [val2]");
-
- underTest.reload(settings);
-
- verifyZeroInteractions(logging);
- verifyZeroInteractions(state);
- verifyZeroInteractions(fs);
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import org.junit.Test;
-import org.sonar.application.cluster.AppStateClusterImpl;
-import org.sonar.application.config.TestAppSettings;
-import org.sonar.process.ProcessProperties;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class AppStateFactoryTest {
-
- private TestAppSettings settings = new TestAppSettings();
- private AppStateFactory underTest = new AppStateFactory(settings);
-
- @Test
- public void create_cluster_implementation_if_cluster_is_enabled() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
- settings.set(ProcessProperties.CLUSTER_NAME, "foo");
-
- AppState appState = underTest.create();
- assertThat(appState).isInstanceOf(AppStateClusterImpl.class);
- ((AppStateClusterImpl) appState).close();
- }
-
- @Test
- public void cluster_implementation_is_disabled_by_default() {
- assertThat(underTest.create()).isInstanceOf(AppStateImpl.class);
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import org.junit.Test;
-import org.sonar.process.ProcessId;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-public class AppStateImplTest {
-
- private AppStateListener listener = mock(AppStateListener.class);
- private AppStateImpl underTest = new AppStateImpl();
-
- @Test
- public void get_and_set_operational_flag() {
- assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isFalse();
- assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isFalse();
- assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isFalse();
-
- underTest.setOperational(ProcessId.ELASTICSEARCH);
-
- assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isFalse();
- assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isTrue();
- assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isFalse();
-
- // only local mode is supported. App state = local state
- assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, false)).isFalse();
- assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, false)).isTrue();
- assertThat(underTest.isOperational(ProcessId.WEB_SERVER, false)).isFalse();
- }
-
- @Test
- public void notify_listeners_when_a_process_becomes_operational() {
- underTest.addListener(listener);
-
- underTest.setOperational(ProcessId.ELASTICSEARCH);
-
- verify(listener).onAppStateOperational(ProcessId.ELASTICSEARCH);
- verifyNoMoreInteractions(listener);
- }
-
- @Test
- public void tryToLockWebLeader_returns_true_if_first_call() {
- assertThat(underTest.tryToLockWebLeader()).isTrue();
-
- // next calls return false
- assertThat(underTest.tryToLockWebLeader()).isFalse();
- assertThat(underTest.tryToLockWebLeader()).isFalse();
- }
-
- @Test
- public void reset_initializes_all_flags() {
- underTest.setOperational(ProcessId.ELASTICSEARCH);
- assertThat(underTest.tryToLockWebLeader()).isTrue();
-
- underTest.reset();
-
- assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isFalse();
- assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isFalse();
- assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isFalse();
- assertThat(underTest.tryToLockWebLeader()).isTrue();
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.io.File;
-import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Ignore;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.DisableOnDebug;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-import org.junit.rules.TestRule;
-import org.junit.rules.Timeout;
-import org.mockito.Mockito;
-import org.sonar.application.config.TestAppSettings;
-import org.sonar.application.process.ProcessLauncher;
-import org.sonar.application.process.ProcessMonitor;
-import org.sonar.process.ProcessId;
-import org.sonar.process.ProcessProperties;
-import org.sonar.process.command.AbstractCommand;
-import org.sonar.process.command.CommandFactory;
-import org.sonar.process.command.EsCommand;
-import org.sonar.process.command.JavaCommand;
-
-import static java.util.Collections.synchronizedList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
-import static org.sonar.process.ProcessId.ELASTICSEARCH;
-import static org.sonar.process.ProcessId.WEB_SERVER;
-
-public class SchedulerImplTest {
-
- @Rule
- public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
- @Rule
- public TemporaryFolder temporaryFolder = new TemporaryFolder();
-
- private EsCommand esCommand;
- private JavaCommand webLeaderCommand;
- private JavaCommand webFollowerCommand;
- private JavaCommand ceCommand;
-
- private AppReloader appReloader = mock(AppReloader.class);
- private TestAppSettings settings = new TestAppSettings();
- private TestCommandFactory javaCommandFactory = new TestCommandFactory();
- private TestProcessLauncher processLauncher = new TestProcessLauncher();
- private TestAppState appState = new TestAppState();
- private List<ProcessId> orderedStops = synchronizedList(new ArrayList<>());
-
- @Before
- public void setUp() throws Exception {
- File tempDir = temporaryFolder.newFolder();
- esCommand = new EsCommand(ELASTICSEARCH, tempDir);
- webLeaderCommand = new JavaCommand(WEB_SERVER, tempDir);
- webFollowerCommand = new JavaCommand(WEB_SERVER, tempDir);
- ceCommand = new JavaCommand(COMPUTE_ENGINE, tempDir);
- }
-
- @After
- public void tearDown() throws Exception {
- processLauncher.close();
- }
-
- @Test
- public void start_and_stop_sequence_of_ES_WEB_CE_in_order() throws Exception {
- enableAllProcesses();
- SchedulerImpl underTest = newScheduler();
- underTest.schedule();
-
- // elasticsearch does not have preconditions to start
- TestProcess es = processLauncher.waitForProcess(ELASTICSEARCH);
- assertThat(es.isAlive()).isTrue();
- assertThat(processLauncher.processes).hasSize(1);
-
- // elasticsearch becomes operational -> web leader is starting
- es.operational = true;
- waitForAppStateOperational(ELASTICSEARCH);
- TestProcess web = processLauncher.waitForProcess(WEB_SERVER);
- assertThat(web.isAlive()).isTrue();
- assertThat(processLauncher.processes).hasSize(2);
- assertThat(processLauncher.commands).containsExactly(esCommand, webLeaderCommand);
-
- // web becomes operational -> CE is starting
- web.operational = true;
- waitForAppStateOperational(WEB_SERVER);
- TestProcess ce = processLauncher.waitForProcess(COMPUTE_ENGINE);
- assertThat(ce.isAlive()).isTrue();
- assertThat(processLauncher.processes).hasSize(3);
- assertThat(processLauncher.commands).containsExactly(esCommand, webLeaderCommand, ceCommand);
-
- // all processes are up
- processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isTrue());
-
- // processes are stopped in reverse order of startup
- underTest.terminate();
- assertThat(orderedStops).containsExactly(COMPUTE_ENGINE, WEB_SERVER, ELASTICSEARCH);
- processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
-
- // does nothing because scheduler is already terminated
- underTest.awaitTermination();
- }
-
- private void enableAllProcesses() {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
- }
-
- @Test
- public void all_processes_are_stopped_if_one_process_goes_down() throws Exception {
- Scheduler underTest = startAll();
-
- processLauncher.waitForProcess(WEB_SERVER).destroyForcibly();
-
- underTest.awaitTermination();
- assertThat(orderedStops).containsExactly(WEB_SERVER, COMPUTE_ENGINE, ELASTICSEARCH);
- processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
-
- // following does nothing
- underTest.terminate();
- underTest.awaitTermination();
- }
-
- @Test
- public void all_processes_are_stopped_if_one_process_fails_to_start() throws Exception {
- enableAllProcesses();
- SchedulerImpl underTest = newScheduler();
- processLauncher.makeStartupFail = COMPUTE_ENGINE;
-
- underTest.schedule();
-
- processLauncher.waitForProcess(ELASTICSEARCH).operational = true;
- processLauncher.waitForProcess(WEB_SERVER).operational = true;
-
- underTest.awaitTermination();
- assertThat(orderedStops).containsExactly(WEB_SERVER, ELASTICSEARCH);
- processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
- }
-
- @Test
- public void terminate_can_be_called_multiple_times() throws Exception {
- Scheduler underTest = startAll();
-
- underTest.terminate();
- processLauncher.processes.values().forEach(p -> assertThat(p.isAlive()).isFalse());
-
- // does nothing
- underTest.terminate();
- }
-
- @Test
- public void awaitTermination_blocks_until_all_processes_are_stopped() throws Exception {
- Scheduler underTest = startAll();
-
- Thread awaitingTermination = new Thread(() -> underTest.awaitTermination());
- awaitingTermination.start();
- assertThat(awaitingTermination.isAlive()).isTrue();
-
- underTest.terminate();
- // the thread is being stopped
- awaitingTermination.join();
- assertThat(awaitingTermination.isAlive()).isFalse();
- }
-
- @Test
- @Ignore("false-positives on Travis CI")
- public void restart_reloads_java_commands_and_restarts_all_processes() throws Exception {
- Scheduler underTest = startAll();
-
- processLauncher.waitForProcess(WEB_SERVER).askedForRestart = true;
-
- // waiting for all processes to be stopped
- boolean stopped = false;
- while (!stopped) {
- stopped = orderedStops.size() == 3;
- Thread.sleep(1L);
- }
-
- // restarting
- verify(appReloader, timeout(60_000)).reload(settings);
- processLauncher.waitForProcessAlive(ELASTICSEARCH);
- processLauncher.waitForProcessAlive(COMPUTE_ENGINE);
- processLauncher.waitForProcessAlive(WEB_SERVER);
-
- underTest.terminate();
- // 3+3 processes have been stopped
- assertThat(orderedStops).hasSize(6);
- assertThat(processLauncher.waitForProcess(ELASTICSEARCH).isAlive()).isFalse();
- assertThat(processLauncher.waitForProcess(COMPUTE_ENGINE).isAlive()).isFalse();
- assertThat(processLauncher.waitForProcess(WEB_SERVER).isAlive()).isFalse();
-
- // verify that awaitTermination() does not block
- underTest.awaitTermination();
- }
-
- @Test
- public void restart_stops_all_if_new_settings_are_not_allowed() throws Exception {
- Scheduler underTest = startAll();
- doThrow(new IllegalStateException("reload error")).when(appReloader).reload(settings);
-
- processLauncher.waitForProcess(WEB_SERVER).askedForRestart = true;
-
- // waiting for all processes to be stopped
- processLauncher.waitForProcessDown(ELASTICSEARCH);
- processLauncher.waitForProcessDown(COMPUTE_ENGINE);
- processLauncher.waitForProcessDown(WEB_SERVER);
-
- // verify that awaitTermination() does not block
- underTest.awaitTermination();
- }
-
- @Test
- public void web_follower_starts_only_when_web_leader_is_operational() throws Exception {
- // leader takes the lock, so underTest won't get it
- assertThat(appState.tryToLockWebLeader()).isTrue();
-
- appState.setOperational(ProcessId.ELASTICSEARCH);
- enableAllProcesses();
- SchedulerImpl underTest = newScheduler();
- underTest.schedule();
-
- processLauncher.waitForProcessAlive(ProcessId.ELASTICSEARCH);
- assertThat(processLauncher.processes).hasSize(1);
-
- // leader becomes operational -> follower can start
- appState.setOperational(ProcessId.WEB_SERVER);
- processLauncher.waitForProcessAlive(WEB_SERVER);
-
- underTest.terminate();
- }
-
- @Test
- public void web_server_waits_for_remote_elasticsearch_to_be_started_if_local_es_is_disabled() throws Exception {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
- settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
- SchedulerImpl underTest = newScheduler();
- underTest.schedule();
-
- // WEB and CE wait for ES to be up
- assertThat(processLauncher.processes).isEmpty();
-
- // ES becomes operational on another node -> web leader can start
- appState.setRemoteOperational(ProcessId.ELASTICSEARCH);
- processLauncher.waitForProcessAlive(WEB_SERVER);
- assertThat(processLauncher.processes).hasSize(1);
-
- underTest.terminate();
- }
-
- @Test
- public void compute_engine_waits_for_remote_elasticsearch_and_web_leader_to_be_started_if_local_es_is_disabled() throws Exception {
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
- settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
- settings.set(ProcessProperties.CLUSTER_WEB_DISABLED, "true");
- SchedulerImpl underTest = newScheduler();
- underTest.schedule();
-
- // CE waits for ES and WEB leader to be up
- assertThat(processLauncher.processes).isEmpty();
-
- // ES and WEB leader become operational on another nodes -> CE can start
- appState.setRemoteOperational(ProcessId.ELASTICSEARCH);
- appState.setRemoteOperational(ProcessId.WEB_SERVER);
-
- processLauncher.waitForProcessAlive(COMPUTE_ENGINE);
- assertThat(processLauncher.processes).hasSize(1);
-
- underTest.terminate();
- }
-
- private SchedulerImpl newScheduler() {
- return new SchedulerImpl(settings, appReloader, javaCommandFactory, processLauncher, appState)
- .setProcessWatcherDelayMs(1L);
- }
-
- private Scheduler startAll() throws InterruptedException {
- enableAllProcesses();
- SchedulerImpl scheduler = newScheduler();
- scheduler.schedule();
- processLauncher.waitForProcess(ELASTICSEARCH).operational = true;
- processLauncher.waitForProcess(WEB_SERVER).operational = true;
- processLauncher.waitForProcess(COMPUTE_ENGINE).operational = true;
- return scheduler;
- }
-
- private void waitForAppStateOperational(ProcessId id) throws InterruptedException {
- while (true) {
- if (appState.isOperational(id, true)) {
- return;
- }
- Thread.sleep(1L);
- }
- }
-
- private class TestCommandFactory implements CommandFactory {
- @Override
- public EsCommand createEsCommand() {
- return esCommand;
- }
-
- @Override
- public JavaCommand createWebCommand(boolean leader) {
- return leader ? webLeaderCommand : webFollowerCommand;
- }
-
- @Override
- public JavaCommand createCeCommand() {
- return ceCommand;
- }
- }
-
- private class TestProcessLauncher implements ProcessLauncher {
- private final EnumMap<ProcessId, TestProcess> processes = new EnumMap<>(ProcessId.class);
- private final List<AbstractCommand<?>> commands = synchronizedList(new ArrayList<>());
- private ProcessId makeStartupFail = null;
-
- @Override
- public ProcessMonitor launch(EsCommand esCommand) {
- return launchImpl(esCommand);
- }
-
- @Override
- public ProcessMonitor launch(JavaCommand javaCommand) {
- return launchImpl(javaCommand);
- }
-
- private ProcessMonitor launchImpl(AbstractCommand<?> javaCommand) {
- commands.add(javaCommand);
- if (makeStartupFail == javaCommand.getProcessId()) {
- throw new IllegalStateException("cannot start " + javaCommand.getProcessId());
- }
- TestProcess process = new TestProcess(javaCommand.getProcessId());
- processes.put(javaCommand.getProcessId(), process);
- return process;
- }
-
- private TestProcess waitForProcess(ProcessId id) throws InterruptedException {
- while (true) {
- TestProcess p = processes.get(id);
- if (p != null) {
- return p;
- }
- Thread.sleep(1L);
- }
- }
-
- private TestProcess waitForProcessAlive(ProcessId id) throws InterruptedException {
- while (true) {
- TestProcess p = processes.get(id);
- if (p != null && p.isAlive()) {
- return p;
- }
- Thread.sleep(1L);
- }
- }
-
- private TestProcess waitForProcessDown(ProcessId id) throws InterruptedException {
- while (true) {
- TestProcess p = processes.get(id);
- if (p != null && !p.isAlive()) {
- return p;
- }
- Thread.sleep(1L);
- }
- }
-
- @Override
- public void close() {
- for (TestProcess process : processes.values()) {
- process.destroyForcibly();
- }
- }
- }
-
- private class TestProcess implements ProcessMonitor, AutoCloseable {
- private final ProcessId processId;
- private final CountDownLatch alive = new CountDownLatch(1);
- private boolean operational = false;
- private boolean askedForRestart = false;
-
- private TestProcess(ProcessId processId) {
- this.processId = processId;
- }
-
- @Override
- public InputStream getInputStream() {
- return mock(InputStream.class, Mockito.RETURNS_MOCKS);
- }
-
- @Override
- public InputStream getErrorStream() {
- return mock(InputStream.class, Mockito.RETURNS_MOCKS);
- }
-
- @Override
- public void closeStreams() {
- }
-
- @Override
- public boolean isAlive() {
- return alive.getCount() == 1;
- }
-
- @Override
- public void askForStop() {
- destroyForcibly();
- }
-
- @Override
- public void destroyForcibly() {
- if (isAlive()) {
- orderedStops.add(processId);
- }
- alive.countDown();
- }
-
- @Override
- public void waitFor() throws InterruptedException {
- alive.await();
- }
-
- @Override
- public void waitFor(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
- alive.await(timeout, timeoutUnit);
- }
-
- @Override
- public boolean isOperational() {
- return operational;
- }
-
- @Override
- public boolean askedForRestart() {
- return askedForRestart;
- }
-
- @Override
- public void acknowledgeAskForRestart() {
- this.askedForRestart = false;
- }
-
- @Override
- public void close() {
- alive.countDown();
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application;
-
-import java.util.ArrayList;
-import java.util.EnumMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.concurrent.atomic.AtomicBoolean;
-import javax.annotation.Nonnull;
-import org.sonar.process.NetworkUtils;
-import org.sonar.process.ProcessId;
-
-public class TestAppState implements AppState {
-
- private final Map<ProcessId, Boolean> localProcesses = new EnumMap<>(ProcessId.class);
- private final Map<ProcessId, Boolean> remoteProcesses = new EnumMap<>(ProcessId.class);
- private final List<AppStateListener> listeners = new ArrayList<>();
- private final AtomicBoolean webLeaderLocked = new AtomicBoolean(false);
-
- @Override
- public void addListener(@Nonnull AppStateListener listener) {
- this.listeners.add(listener);
- }
-
- @Override
- public boolean isOperational(ProcessId processId, boolean local) {
- if (local) {
- return localProcesses.computeIfAbsent(processId, p -> false);
- }
- return remoteProcesses.computeIfAbsent(processId, p -> false);
- }
-
- @Override
- public void setOperational(ProcessId processId) {
- localProcesses.put(processId, true);
- remoteProcesses.put(processId, true);
- listeners.forEach(l -> l.onAppStateOperational(processId));
- }
-
- public void setRemoteOperational(ProcessId processId) {
- remoteProcesses.put(processId, true);
- listeners.forEach(l -> l.onAppStateOperational(processId));
- }
-
- @Override
- public boolean tryToLockWebLeader() {
- return webLeaderLocked.compareAndSet(false, true);
- }
-
- @Override
- public void reset() {
- webLeaderLocked.set(false);
- localProcesses.clear();
- }
-
- @Override
- public void registerSonarQubeVersion(String sonarqubeVersion) {
- // nothing to do
- }
-
- @Override
- public Optional<String> getLeaderHostName() {
- return Optional.of(NetworkUtils.getHostName());
- }
-
- @Override
- public void close() {
- // nothing to do
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.cluster;
-
-import com.hazelcast.core.HazelcastInstance;
-import java.io.IOException;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.DisableOnDebug;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TestRule;
-import org.junit.rules.Timeout;
-import org.slf4j.Logger;
-import org.sonar.application.AppStateListener;
-import org.sonar.application.config.TestAppSettings;
-import org.sonar.process.ProcessId;
-import org.sonar.process.ProcessProperties;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Matchers.eq;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.sonar.application.cluster.HazelcastTestHelper.createHazelcastClient;
-import static org.sonar.application.cluster.HazelcastTestHelper.newClusterSettings;
-import static org.sonar.process.cluster.ClusterObjectKeys.SONARQUBE_VERSION;
-
-public class AppStateClusterImplTest {
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- @Rule
- public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
-
- @Test
- public void instantiation_throws_ISE_if_cluster_mode_is_disabled() throws Exception {
- TestAppSettings settings = new TestAppSettings();
- settings.set(ProcessProperties.CLUSTER_ENABLED, "false");
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Cluster is not enabled on this instance");
-
- new AppStateClusterImpl(settings);
- }
-
- @Test
- public void tryToLockWebLeader_returns_true_only_for_the_first_call() throws Exception {
- TestAppSettings settings = newClusterSettings();
-
- try (AppStateClusterImpl underTest = new AppStateClusterImpl(settings)) {
- assertThat(underTest.tryToLockWebLeader()).isEqualTo(true);
- assertThat(underTest.tryToLockWebLeader()).isEqualTo(false);
- }
- }
-
- @Test
- public void log_when_sonarqube_is_joining_a_cluster () throws IOException, InterruptedException, IllegalAccessException, NoSuchFieldException {
- // Now launch an instance that try to be part of the hzInstance cluster
- TestAppSettings settings = newClusterSettings();
-
- Logger logger = mock(Logger.class);
- AppStateClusterImpl.setLogger(logger);
-
- try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
- verify(logger).info(
- eq("Joined the cluster [{}] that contains the following hosts : [{}]"),
- eq("sonarqube"),
- anyString()
- );
- }
- }
-
- @Test
- public void test_listeners() throws InterruptedException {
- AppStateListener listener = mock(AppStateListener.class);
- try (AppStateClusterImpl underTest = new AppStateClusterImpl(newClusterSettings())) {
- underTest.addListener(listener);
-
- underTest.setOperational(ProcessId.ELASTICSEARCH);
- verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
-
- assertThat(underTest.isOperational(ProcessId.ELASTICSEARCH, true)).isEqualTo(true);
- assertThat(underTest.isOperational(ProcessId.APP, true)).isEqualTo(false);
- assertThat(underTest.isOperational(ProcessId.WEB_SERVER, true)).isEqualTo(false);
- assertThat(underTest.isOperational(ProcessId.COMPUTE_ENGINE, true)).isEqualTo(false);
- }
- }
-
- @Test
- public void registerSonarQubeVersion_publishes_version_on_first_call() {
- TestAppSettings settings = newClusterSettings();
-
- try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
- appStateCluster.registerSonarQubeVersion("6.4.1.5");
-
- HazelcastInstance hzInstance = createHazelcastClient(appStateCluster);
- assertThat(hzInstance.getAtomicReference(SONARQUBE_VERSION).get())
- .isNotNull()
- .isInstanceOf(String.class)
- .isEqualTo("6.4.1.5");
- }
- }
-
- @Test
- public void reset_throws_always_ISE() {
- TestAppSettings settings = newClusterSettings();
-
- try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("state reset is not supported in cluster mode");
- appStateCluster.reset();
- }
- }
-
- @Test
- public void registerSonarQubeVersion_throws_ISE_if_initial_version_is_different() throws Exception {
- // Now launch an instance that try to be part of the hzInstance cluster
- TestAppSettings settings = newClusterSettings();
-
- try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
- // Register first version
- appStateCluster.registerSonarQubeVersion("1.0.0");
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("The local version 2.0.0 is not the same as the cluster 1.0.0");
-
- // Registering a second different version must trigger an exception
- appStateCluster.registerSonarQubeVersion("2.0.0");
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.cluster;
-
-import org.junit.Test;
-import org.sonar.process.ProcessId;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ClusterProcessTest {
- @Test
- public void test_equality() {
- ClusterProcess clusterProcess = new ClusterProcess("A", ProcessId.WEB_SERVER);
-
- assertThat(clusterProcess)
- .isNotEqualTo(null)
- .isEqualTo(clusterProcess)
- .isNotEqualTo(new ClusterProcess("B", ProcessId.WEB_SERVER))
- .isNotEqualTo(new ClusterProcess("A", ProcessId.ELASTICSEARCH))
- .isEqualTo(new ClusterProcess("A", ProcessId.WEB_SERVER));
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.cluster;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.application.config.AppSettings;
-import org.sonar.application.config.TestAppSettings;
-import org.sonar.process.ProcessProperties;
-
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.stream.Stream;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ClusterPropertiesTest {
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- private AppSettings appSettings = new TestAppSettings();
-
- @Test
- public void test_default_values() throws Exception {
- ClusterProperties props = new ClusterProperties(appSettings);
-
- assertThat(props.getNetworkInterfaces())
- .isEqualTo(Collections.emptyList());
- assertThat(props.getPort())
- .isEqualTo(9003);
- assertThat(props.isEnabled())
- .isEqualTo(false);
- assertThat(props.getHosts())
- .isEqualTo(Collections.emptyList());
- assertThat(props.getName())
- .isEqualTo("sonarqube");
- }
-
- @Test
- public void test_port_parameter() {
- appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
- appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
-
- Stream.of("-50", "0", "65536", "128563").forEach(
- port -> {
- appSettings.getProps().set(ProcessProperties.CLUSTER_PORT, port);
-
- ClusterProperties clusterProperties = new ClusterProperties(appSettings);
- expectedException.expect(IllegalArgumentException.class);
- expectedException.expectMessage(
- String.format("Cluster port have been set to %s which is outside the range [1-65535].", port));
- clusterProperties.validate();
-
- });
- }
-
- @Test
- public void test_interfaces_parameter() {
- appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
- appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
- appSettings.getProps().set(ProcessProperties.CLUSTER_NETWORK_INTERFACES, "8.8.8.8"); // This IP belongs to Google
-
- ClusterProperties clusterProperties = new ClusterProperties(appSettings);
- expectedException.expect(IllegalArgumentException.class);
- expectedException.expectMessage(
- String.format("Interface %s is not available on this machine.", "8.8.8.8"));
- clusterProperties.validate();
- }
-
- @Test
- public void validate_does_not_fail_if_cluster_enabled_and_name_specified() {
- appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
- appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
-
- ClusterProperties clusterProperties = new ClusterProperties(appSettings);
- clusterProperties.validate();
- }
-
- @Test
- public void test_members() {
- appSettings.getProps().set(ProcessProperties.CLUSTER_ENABLED, "true");
- appSettings.getProps().set(ProcessProperties.CLUSTER_NAME, "sonarqube");
-
- assertThat(
- new ClusterProperties(appSettings).getHosts()).isEqualTo(
- Collections.emptyList());
-
- appSettings.getProps().set(ProcessProperties.CLUSTER_HOSTS, "192.168.1.1");
- assertThat(
- new ClusterProperties(appSettings).getHosts()).isEqualTo(
- Arrays.asList("192.168.1.1:9003"));
-
- appSettings.getProps().set(ProcessProperties.CLUSTER_HOSTS, "192.168.1.2:5501");
- assertThat(
- new ClusterProperties(appSettings).getHosts()).containsExactlyInAnyOrder(
- "192.168.1.2:5501");
-
- appSettings.getProps().set(ProcessProperties.CLUSTER_HOSTS, "192.168.1.2:5501,192.168.1.1");
- assertThat(
- new ClusterProperties(appSettings).getHosts()).containsExactlyInAnyOrder(
- "192.168.1.2:5501", "192.168.1.1:9003");
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.cluster;
-
-import ch.qos.logback.classic.LoggerContext;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.AppenderBase;
-import com.hazelcast.core.HazelcastInstance;
-import com.hazelcast.core.ItemEvent;
-import com.hazelcast.core.ItemListener;
-import com.hazelcast.core.ReplicatedMap;
-import java.net.InetAddress;
-import java.util.AbstractMap;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.UUID;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import org.junit.AfterClass;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.DisableOnDebug;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TestRule;
-import org.junit.rules.Timeout;
-import org.slf4j.LoggerFactory;
-import org.sonar.application.AppStateListener;
-import org.sonar.application.config.TestAppSettings;
-import org.sonar.process.NetworkUtils;
-import org.sonar.process.ProcessId;
-import org.sonar.process.ProcessProperties;
-import org.sonar.process.cluster.ClusterObjectKeys;
-
-import static junit.framework.TestCase.fail;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.sonar.application.cluster.HazelcastTestHelper.closeAllHazelcastClients;
-import static org.sonar.application.cluster.HazelcastTestHelper.createHazelcastClient;
-import static org.sonar.application.cluster.HazelcastTestHelper.newClusterSettings;
-import static org.sonar.process.ProcessProperties.CLUSTER_NAME;
-import static org.sonar.process.cluster.ClusterObjectKeys.LEADER;
-import static org.sonar.process.cluster.ClusterObjectKeys.OPERATIONAL_PROCESSES;
-import static org.sonar.process.cluster.ClusterObjectKeys.SONARQUBE_VERSION;
-
-public class HazelcastClusterTest {
- @Rule
- public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- @AfterClass
- public static void closeHazelcastClients() {
- closeAllHazelcastClients();
- }
-
- @Test
- public void test_two_tryToLockWebLeader_must_return_true() {
- ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- assertThat(hzCluster.tryToLockWebLeader()).isEqualTo(true);
- assertThat(hzCluster.tryToLockWebLeader()).isEqualTo(false);
- }
- }
-
- @Test
- public void when_another_process_locked_webleader_tryToLockWebLeader_must_return_false() {
- ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
- hzInstance.getAtomicReference(LEADER).set("aaaa");
- assertThat(hzCluster.tryToLockWebLeader()).isEqualTo(false);
- }
- }
-
- @Test
- public void when_no_leader_getLeaderHostName_must_return_NO_LEADER() {
- ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- assertThat(hzCluster.getLeaderHostName()).isEmpty();
- }
- }
-
- @Test
- public void when_no_leader_getLeaderHostName_must_return_the_hostname() {
- ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- assertThat(hzCluster.tryToLockWebLeader()).isTrue();
- assertThat(hzCluster.getLeaderHostName().get()).isEqualTo(NetworkUtils.getHostName());
- }
- }
-
- @Test
- public void members_must_be_empty_when_there_is_no_other_node() {
- ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- assertThat(hzCluster.getMembers()).isEmpty();
- }
- }
-
- @Test
- public void set_operational_is_writing_to_cluster() {
- ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
-
- hzCluster.setOperational(ProcessId.ELASTICSEARCH);
-
- assertThat(hzCluster.isOperational(ProcessId.ELASTICSEARCH)).isTrue();
- assertThat(hzCluster.isOperational(ProcessId.WEB_SERVER)).isFalse();
- assertThat(hzCluster.isOperational(ProcessId.COMPUTE_ENGINE)).isFalse();
-
- // Connect via Hazelcast client to test values
- HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
- ReplicatedMap<ClusterProcess, Boolean> operationalProcesses = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
- assertThat(operationalProcesses)
- .containsExactly(new AbstractMap.SimpleEntry<>(new ClusterProcess(hzCluster.getLocalUUID(), ProcessId.ELASTICSEARCH), Boolean.TRUE));
- }
- }
-
- @Test
- public void cluster_name_comes_from_configuration() {
- TestAppSettings testAppSettings = newClusterSettings();
- testAppSettings.set(CLUSTER_NAME, "a_cluster_");
- ClusterProperties clusterProperties = new ClusterProperties(testAppSettings);
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- assertThat(hzCluster.getName()).isEqualTo("a_cluster_");
- }
- }
-
- @Test
- public void cluster_must_keep_a_list_of_clients() throws InterruptedException {
- TestAppSettings testAppSettings = newClusterSettings();
- testAppSettings.set(CLUSTER_NAME, "a_cluster_");
- ClusterProperties clusterProperties = new ClusterProperties(testAppSettings);
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- assertThat(hzCluster.hzInstance.getSet(ClusterObjectKeys.CLIENT_UUIDS)).isEmpty();
- HazelcastInstance hzClient = HazelcastTestHelper.createHazelcastClient(hzCluster);
- assertThat(hzCluster.hzInstance.getSet(ClusterObjectKeys.CLIENT_UUIDS)).containsExactly(hzClient.getLocalEndpoint().getUuid());
-
- CountDownLatch latch = new CountDownLatch(1);
- hzCluster.hzInstance.getSet(ClusterObjectKeys.CLIENT_UUIDS).addItemListener(new ItemListener<Object>() {
- @Override
- public void itemAdded(ItemEvent<Object> item) {
- }
-
- @Override
- public void itemRemoved(ItemEvent<Object> item) {
- latch.countDown();
- }
- }, false);
-
- hzClient.shutdown();
- if (latch.await(5, TimeUnit.SECONDS)) {
- assertThat(hzCluster.hzInstance.getSet(ClusterObjectKeys.CLIENT_UUIDS).size()).isEqualTo(0);
- } else {
- fail("The client UUID have not been removed from the Set within 5 seconds' time lapse");
- }
- }
- }
-
- @Test
- public void localUUID_must_not_be_empty() {
- ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- assertThat(hzCluster.getLocalUUID()).isNotEmpty();
- }
- }
-
- @Test
- public void when_a_process_is_set_operational_listener_must_be_triggered() {
- ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- AppStateListener listener = mock(AppStateListener.class);
- hzCluster.addListener(listener);
-
- // ElasticSearch is not operational
- assertThat(hzCluster.isOperational(ProcessId.ELASTICSEARCH)).isFalse();
-
- // Simulate a node that set ElasticSearch operational
- HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
- ReplicatedMap<ClusterProcess, Boolean> operationalProcesses = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
- operationalProcesses.put(new ClusterProcess(UUID.randomUUID().toString(), ProcessId.ELASTICSEARCH), Boolean.TRUE);
- verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
- verifyNoMoreInteractions(listener);
-
- // ElasticSearch is operational
- assertThat(hzCluster.isOperational(ProcessId.ELASTICSEARCH)).isTrue();
- }
- }
-
-
- @Test
- public void registerSonarQubeVersion_publishes_version_on_first_call() {
- ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- hzCluster.registerSonarQubeVersion("1.0.0.0");
-
- HazelcastInstance hzInstance = createHazelcastClient(hzCluster);
- assertThat(hzInstance.getAtomicReference(SONARQUBE_VERSION).get()).isEqualTo("1.0.0.0");
- }
- }
-
- @Test
- public void registerSonarQubeVersion_throws_ISE_if_initial_version_is_different() throws Exception {
- ClusterProperties clusterProperties = new ClusterProperties(newClusterSettings());
- try (HazelcastCluster hzCluster = HazelcastCluster.create(clusterProperties)) {
- // Register first version
- hzCluster.registerSonarQubeVersion("1.0.0");
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("The local version 2.0.0 is not the same as the cluster 1.0.0");
-
- // Registering a second different version must trigger an exception
- hzCluster.registerSonarQubeVersion("2.0.0");
- }
- }
-
- @Test
- public void simulate_network_cluster() throws InterruptedException {
- TestAppSettings settings = newClusterSettings();
- settings.set(ProcessProperties.CLUSTER_NETWORK_INTERFACES, InetAddress.getLoopbackAddress().getHostAddress());
- AppStateListener listener = mock(AppStateListener.class);
-
- try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(settings)) {
- appStateCluster.addListener(listener);
-
- HazelcastInstance hzInstance = createHazelcastClient(appStateCluster.getHazelcastCluster());
- String uuid = UUID.randomUUID().toString();
- ReplicatedMap<ClusterProcess, Boolean> replicatedMap = hzInstance.getReplicatedMap(OPERATIONAL_PROCESSES);
- // process is not up yet --> no events are sent to listeners
- replicatedMap.put(
- new ClusterProcess(uuid, ProcessId.ELASTICSEARCH),
- Boolean.FALSE);
-
- // process is up yet --> notify listeners
- replicatedMap.replace(
- new ClusterProcess(uuid, ProcessId.ELASTICSEARCH),
- Boolean.TRUE);
-
- // should be called only once
- verify(listener, timeout(20_000)).onAppStateOperational(ProcessId.ELASTICSEARCH);
- verifyNoMoreInteractions(listener);
-
- hzInstance.shutdown();
- }
- }
-
- @Test
- public void hazelcast_must_log_through_sl4fj() {
- MemoryAppender<ILoggingEvent> memoryAppender = new MemoryAppender<>();
- LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
- lc.reset();
- memoryAppender.setContext(lc);
- memoryAppender.start();
- lc.getLogger("com.hazelcast").addAppender(memoryAppender);
-
- try (AppStateClusterImpl appStateCluster = new AppStateClusterImpl(newClusterSettings())) {
- }
-
- assertThat(memoryAppender.events).isNotEmpty();
- memoryAppender.events.stream().forEach(
- e -> assertThat(e.getLoggerName()).startsWith("com.hazelcast")
- );
- }
-
- private class MemoryAppender<E> extends AppenderBase<E> {
- private final List<E> events = new ArrayList();
-
- @Override
- protected void append(E eventObject) {
- events.add(eventObject);
- }
- }
-
-
- @Test
- public void configuration_tweaks_of_hazelcast_must_be_present() {
- try (HazelcastCluster hzCluster = HazelcastCluster.create(new ClusterProperties(newClusterSettings()))) {
- assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.tcp.join.port.try.count")).isEqualTo("10");
- assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.phone.home.enabled")).isEqualTo("false");
- assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.logging.type")).isEqualTo("slf4j");
- assertThat(hzCluster.hzInstance.getConfig().getProperty("hazelcast.socket.bind.any")).isEqualTo("false");
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.cluster;
-
-import com.hazelcast.client.HazelcastClient;
-import com.hazelcast.client.config.ClientConfig;
-import com.hazelcast.core.HazelcastInstance;
-import java.net.InetSocketAddress;
-import java.util.ArrayList;
-import java.util.List;
-import org.sonar.application.config.TestAppSettings;
-import org.sonar.process.ProcessProperties;
-
-public class HazelcastTestHelper {
-
- // Be careful this test won't work if parallel tests is used
- private static final List<HazelcastInstance> HAZELCAST_INSTANCES = new ArrayList<>();
-
- static HazelcastInstance createHazelcastClient(HazelcastCluster hzCluster) {
- ClientConfig clientConfig = new ClientConfig();
- InetSocketAddress socketAddress = (InetSocketAddress) hzCluster.hzInstance.getLocalEndpoint().getSocketAddress();
-
- clientConfig.getNetworkConfig().getAddresses().add(
- String.format("%s:%d",
- socketAddress.getHostString(),
- socketAddress.getPort()
- ));
- clientConfig.getGroupConfig().setName(hzCluster.getName());
- HazelcastInstance hazelcastInstance = HazelcastClient.newHazelcastClient(clientConfig);
- HAZELCAST_INSTANCES.add(hazelcastInstance);
- return hazelcastInstance;
- }
-
- static void closeAllHazelcastClients() {
- HAZELCAST_INSTANCES.stream().forEach(
- hz -> {
- try {
- hz.shutdown();
- } catch (Exception ex) {
- // Ignore it
- }
- }
- );
- }
-
- static HazelcastInstance createHazelcastClient(AppStateClusterImpl appStateCluster) {
- return createHazelcastClient(appStateCluster.getHazelcastCluster());
- }
-
- static TestAppSettings newClusterSettings() {
- TestAppSettings settings = new TestAppSettings();
- settings.set(ProcessProperties.CLUSTER_ENABLED, "true");
- settings.set(ProcessProperties.CLUSTER_NAME, "sonarqube");
- return settings;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import java.util.Properties;
-import org.junit.Test;
-import org.sonar.process.Props;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class AppSettingsImplTest {
-
- @Test
- public void reload_updates_properties() {
- Props initialProps = new Props(new Properties());
- initialProps.set("foo", "bar");
- Props newProps = new Props(new Properties());
- newProps.set("foo", "baz");
- newProps.set("newProp", "newVal");
-
- AppSettingsImpl underTest = new AppSettingsImpl(initialProps);
- underTest.reload(newProps);
-
- assertThat(underTest.getValue("foo").get()).isEqualTo("baz");
- assertThat(underTest.getValue("newProp").get()).isEqualTo("newVal");
- assertThat(underTest.getProps().rawProperties()).hasSize(2);
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import java.io.File;
-import org.apache.commons.io.FileUtils;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.data.MapEntry.entry;
-
-public class AppSettingsLoaderImplTest {
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
-
- @Test
- public void load_properties_from_file() throws Exception {
- File homeDir = temp.newFolder();
- File propsFile = new File(homeDir, "conf/sonar.properties");
- FileUtils.write(propsFile, "foo=bar");
-
- AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir);
- AppSettings settings = underTest.load();
-
- assertThat(settings.getProps().rawProperties()).contains(entry("foo", "bar"));
- }
-
- @Test
- public void throws_ISE_if_file_fails_to_be_loaded() throws Exception {
- File homeDir = temp.newFolder();
- File propsFileAsDir = new File(homeDir, "conf/sonar.properties");
- FileUtils.forceMkdir(propsFileAsDir);
- AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir);
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Cannot open file " + propsFileAsDir.getAbsolutePath());
-
- underTest.load();
- }
-
- @Test
- public void file_is_not_loaded_if_it_does_not_exist() throws Exception {
- File homeDir = temp.newFolder();
-
- AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[0], homeDir);
- AppSettings settings = underTest.load();
-
- // no failure, file is ignored
- assertThat(settings.getProps()).isNotNull();
- }
-
- @Test
- public void command_line_arguments_are_included_to_settings() throws Exception {
- File homeDir = temp.newFolder();
-
- AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[] {"-Dsonar.foo=bar", "-Dhello=world"}, homeDir);
- AppSettings settings = underTest.load();
-
- assertThat(settings.getProps().rawProperties())
- .contains(entry("sonar.foo", "bar"))
- .contains(entry("hello", "world"));
- }
-
- @Test
- public void command_line_arguments_make_precedence_over_properties_files() throws Exception {
- File homeDir = temp.newFolder();
- File propsFile = new File(homeDir, "conf/sonar.properties");
- FileUtils.write(propsFile, "sonar.foo=file");
-
- AppSettingsLoaderImpl underTest = new AppSettingsLoaderImpl(new String[]{"-Dsonar.foo=cli"}, homeDir);
- AppSettings settings = underTest.load();
-
- assertThat(settings.getProps().rawProperties()).contains(entry("sonar.foo", "cli"));
- }
-
- @Test
- public void detectHomeDir_returns_existing_dir() throws Exception {
- assertThat(new AppSettingsLoaderImpl(new String[0]).getHomeDir()).exists().isDirectory();
-
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.config;
-
-import java.net.NetworkInterface;
-import java.net.SocketException;
-import java.util.Collections;
-import java.util.Enumeration;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.experimental.theories.DataPoints;
-import org.junit.experimental.theories.FromDataPoints;
-import org.junit.experimental.theories.Theories;
-import org.junit.experimental.theories.Theory;
-import org.junit.rules.ExpectedException;
-import org.junit.runner.RunWith;
-import org.sonar.process.MessageException;
-
-import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
-import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
-import static org.sonar.process.ProcessProperties.CLUSTER_NETWORK_INTERFACES;
-import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
-import static org.sonar.process.ProcessProperties.JDBC_URL;
-import static org.sonar.process.ProcessProperties.SEARCH_HOST;
-
-@RunWith(Theories.class)
-public class ClusterSettingsLoopbackTest {
-
- private TestAppSettings settings;
- private static final String LOOPBACK_FORBIDDEN = " must not be a loopback address";
- private static final String NOT_LOCAL_ADDRESS = " is not a local address";
- private static final String NOT_RESOLVABLE = " cannot be resolved";
-
- @DataPoints("parameter")
- public static final ValueAndResult[] VALID_SINGLE_IP = {
- // Valid IPs
- new ValueAndResult("1.2.3.4", NOT_LOCAL_ADDRESS),
- new ValueAndResult("1.2.3.4:9001", NOT_LOCAL_ADDRESS),
- new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb", NOT_LOCAL_ADDRESS),
- new ValueAndResult("[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001", NOT_LOCAL_ADDRESS),
-
- // Valid Name
- new ValueAndResult("www.sonarqube.org", NOT_LOCAL_ADDRESS),
- new ValueAndResult("www.google.fr", NOT_LOCAL_ADDRESS),
- new ValueAndResult("www.google.com, www.sonarsource.com, wwww.sonarqube.org", NOT_LOCAL_ADDRESS),
-
- new ValueAndResult("...", NOT_RESOLVABLE),
- new ValueAndResult("භඦආ\uD801\uDC8C\uD801\uDC8B", NOT_RESOLVABLE),
-
- // Valide IPs List
- new ValueAndResult("1.2.3.4,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb", NOT_LOCAL_ADDRESS),
- new ValueAndResult("1.2.3.4:9001,[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001", NOT_LOCAL_ADDRESS),
- new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,1.2.3.4:9001", NOT_LOCAL_ADDRESS),
- new ValueAndResult("[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccc", NOT_LOCAL_ADDRESS),
-
- // Loopback IPs
- new ValueAndResult("localhost", LOOPBACK_FORBIDDEN),
- new ValueAndResult("127.0.0.1", LOOPBACK_FORBIDDEN),
- new ValueAndResult("127.1.1.1", LOOPBACK_FORBIDDEN),
- new ValueAndResult("127.243.136.241", LOOPBACK_FORBIDDEN),
- new ValueAndResult("::1", LOOPBACK_FORBIDDEN),
- new ValueAndResult("0:0:0:0:0:0:0:1", LOOPBACK_FORBIDDEN),
- new ValueAndResult("localhost:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("127.0.0.1:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("127.1.1.1:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("127.243.136.241:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("[::1]:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("[0:0:0:0:0:0:0:1]:9001", LOOPBACK_FORBIDDEN),
-
- // Loopback IPs list
- new ValueAndResult("127.0.0.1,192.168.11.25", LOOPBACK_FORBIDDEN),
- new ValueAndResult("192.168.11.25,127.1.1.1", LOOPBACK_FORBIDDEN),
- new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,0:0:0:0:0:0:0:1", LOOPBACK_FORBIDDEN),
- new ValueAndResult("0:0:0:0:0:0:0:1,2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb", LOOPBACK_FORBIDDEN),
- new ValueAndResult("2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb,::1", LOOPBACK_FORBIDDEN),
- new ValueAndResult("::1,2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb", LOOPBACK_FORBIDDEN),
- new ValueAndResult("::1,2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb,2a01:e34:ef1f:dbb0:b3f6:a978:c5c0:9ccb", LOOPBACK_FORBIDDEN),
- new ValueAndResult("localhost:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("127.0.0.1:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("127.1.1.1:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("127.243.136.241:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("[::1]:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("[0:0:0:0:0:0:0:1]:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("127.0.0.1,192.168.11.25:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("192.168.11.25:9001,127.1.1.1:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb,[0:0:0:0:0:0:0:1]:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("[0:0:0:0:0:0:0:1]:9001,[2a01:e34:ef1f:dbb0:c2f6:a978:c5c0:9ccb]:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("[2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb]:9001,[::1]:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("[::1]:9001,[2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb]:9001", LOOPBACK_FORBIDDEN),
- new ValueAndResult("[::1]:9001,[2a01:e34:ef1f:dbb0:c3f6:a978:c5c0:9ccb]:9001,[2a01:e34:ef1f:dbb0:b3f6:a978:c5c0:9ccb]:9001", LOOPBACK_FORBIDDEN)
- };
-
- @DataPoints("key")
- public static final Key[] KEYS = {
- new Key(SEARCH_HOST, false, false),
- new Key(CLUSTER_NETWORK_INTERFACES, true, false),
- new Key(CLUSTER_SEARCH_HOSTS, true, true),
- new Key(CLUSTER_HOSTS, true, true)
- };
-
-
- @DataPoints("unresolvable_hosts")
- public static final String[] UNRESOLVABLE_HOSTS = {
- };
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- @Before
- public void resetSettings() {
- settings = getClusterSettings();
- }
-
- @Theory
- public void accept_throws_MessageException(
- @FromDataPoints("key") Key propertyKey,
- @FromDataPoints("parameter") ValueAndResult valueAndResult) {
- // Skip the test if the value is a list and if the key is not accepting a list
- if (settings == null) {
- System.out.println("No network found, skipping the test");
- return;
- }
- if ((valueAndResult.isList() && propertyKey.acceptList) || !valueAndResult.isList()) {
- settings.set(propertyKey.getKey(), valueAndResult.getValue());
-
- // If the key accept non local IPs there won't be any exception
- if (!propertyKey.acceptNonLocal || valueAndResult.getMessage() != NOT_LOCAL_ADDRESS) {
- expectedException.expect(MessageException.class);
- expectedException.expectMessage(valueAndResult.getMessage());
- }
-
- new ClusterSettings().accept(settings.getProps());
- }
- }
-
- private static TestAppSettings getClusterSettings() {
- String localAddress = null;
- try {
- Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
- for (NetworkInterface networkInterface : Collections.list(nets)) {
- if (!networkInterface.isLoopback() && networkInterface.isUp()) {
- localAddress = networkInterface.getInetAddresses().nextElement().getHostAddress();
- }
- }
- if (localAddress == null) {
- return null;
- }
-
- } catch (SocketException e) {
- return null;
- }
-
- TestAppSettings testAppSettings = new TestAppSettings()
- .set(CLUSTER_ENABLED, "true")
- .set(CLUSTER_SEARCH_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
- .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
- .set(SEARCH_HOST, localAddress)
- .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
- return testAppSettings;
- }
-
- private static class Key {
- private final String key;
- private final boolean acceptList;
- private final boolean acceptNonLocal;
-
- private Key(String key, boolean acceptList, boolean acceptNonLocal) {
- this.key = key;
- this.acceptList = acceptList;
- this.acceptNonLocal = acceptNonLocal;
- }
-
- public String getKey() {
- return key;
- }
-
- public boolean acceptList() {
- return acceptList;
- }
-
- public boolean acceptNonLocal() {
- return acceptNonLocal;
- }
- }
-
- private static class ValueAndResult {
- private final String value;
- private final String message;
-
- private ValueAndResult(String value, String message) {
- this.value = value;
- this.message = message;
- }
-
- public String getValue() {
- return value;
- }
-
- public String getMessage() {
- return message;
- }
-
- public boolean isList() {
- return value != null && value.contains(",");
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.process.MessageException;
-import org.sonar.process.ProcessProperties;
-
-import static java.lang.String.format;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.process.ProcessId.COMPUTE_ENGINE;
-import static org.sonar.process.ProcessId.ELASTICSEARCH;
-import static org.sonar.process.ProcessId.WEB_SERVER;
-import static org.sonar.process.ProcessProperties.CLUSTER_ENABLED;
-import static org.sonar.process.ProcessProperties.CLUSTER_HOSTS;
-import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_DISABLED;
-import static org.sonar.process.ProcessProperties.CLUSTER_SEARCH_HOSTS;
-import static org.sonar.process.ProcessProperties.JDBC_URL;
-import static org.sonar.process.ProcessProperties.SEARCH_HOST;
-
-
-public class ClusterSettingsTest {
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- private TestAppSettings settings;
-
- @Before
- public void resetSettings() {
- settings = getClusterSettings();
- }
-
- @Test
- public void test_isClusterEnabled() {
- settings.set(CLUSTER_ENABLED, "true");
- assertThat(ClusterSettings.isClusterEnabled(settings)).isTrue();
-
- settings.set(CLUSTER_ENABLED, "false");
- assertThat(ClusterSettings.isClusterEnabled(settings)).isFalse();
- }
-
- @Test
- public void isClusterEnabled_returns_false_by_default() {
- assertThat(ClusterSettings.isClusterEnabled(new TestAppSettings())).isFalse();
- }
-
- @Test
- public void getEnabledProcesses_returns_all_processes_by_default() {
- assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, ELASTICSEARCH, WEB_SERVER);
- }
-
- @Test
- public void getEnabledProcesses_returns_all_processes_by_default_in_cluster_mode() {
- settings.set(CLUSTER_ENABLED, "true");
-
- assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, ELASTICSEARCH, WEB_SERVER);
- }
-
- @Test
- public void getEnabledProcesses_returns_configured_processes_in_cluster_mode() {
- settings.set(CLUSTER_ENABLED, "true");
- settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
-
- assertThat(ClusterSettings.getEnabledProcesses(settings)).containsOnly(COMPUTE_ENGINE, WEB_SERVER);
- }
-
- @Test
- public void accept_throws_MessageException_if_internal_property_for_web_leader_is_configured() {
- settings.set(CLUSTER_ENABLED, "true");
- settings.set("sonar.cluster.web.startupLeader", "true");
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Property [sonar.cluster.web.startupLeader] is forbidden");
-
- new ClusterSettings().accept(settings.getProps());
- }
-
- @Test
- public void accept_throws_MessageException_if_search_enabled_with_loopback() {
- settings.set(CLUSTER_ENABLED, "true");
- settings.set(CLUSTER_SEARCH_DISABLED, "false");
- settings.set(CLUSTER_SEARCH_HOSTS, "192.168.1.1,192.168.1.2");
- settings.set(SEARCH_HOST, "::1");
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("The interface address [::1] of [sonar.search.host] must not be a loopback address");
-
- new ClusterSettings().accept(settings.getProps());
- }
-
- @Test
- public void accept_not_throwing_MessageException_if_search_disabled_with_loopback() {
- settings.set(CLUSTER_ENABLED, "true");
- settings.set(CLUSTER_SEARCH_DISABLED, "true");
- settings.set(CLUSTER_SEARCH_HOSTS, "192.168.1.1,192.168.1.2");
- settings.set(SEARCH_HOST, "127.0.0.1");
-
- new ClusterSettings().accept(settings.getProps());
- }
-
- @Test
- public void accept_does_nothing_if_cluster_is_disabled() {
- settings.set(CLUSTER_ENABLED, "false");
- // this property is supposed to fail if cluster is enabled
- settings.set("sonar.cluster.web.startupLeader", "true");
-
- new ClusterSettings().accept(settings.getProps());
- }
-
- @Test
- public void accept_throws_MessageException_if_h2() {
- settings.set(CLUSTER_ENABLED, "true");
- settings.set("sonar.jdbc.url", "jdbc:h2:mem");
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Embedded database is not supported in cluster mode");
-
- new ClusterSettings().accept(settings.getProps());
- }
-
- @Test
- public void accept_throws_MessageException_if_default_jdbc_url() {
- settings.clearProperty(JDBC_URL);
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Embedded database is not supported in cluster mode");
-
- new ClusterSettings().accept(settings.getProps());
- }
-
- @Test
- public void isLocalElasticsearchEnabled_returns_true_by_default() {
- assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isTrue();
- }
-
- @Test
- public void isLocalElasticsearchEnabled_returns_true_by_default_in_cluster_mode() {
- assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isTrue();
- }
-
- @Test
- public void isLocalElasticsearchEnabled_returns_false_if_local_es_node_is_disabled_in_cluster_mode() {
- settings.set(CLUSTER_ENABLED, "true");
- settings.set(ProcessProperties.CLUSTER_SEARCH_DISABLED, "true");
-
- assertThat(ClusterSettings.isLocalElasticsearchEnabled(settings)).isFalse();
- }
-
- @Test
- public void accept_throws_MessageException_if_searchHost_is_missing() {
- settings.clearProperty(SEARCH_HOST);
- checkMandatoryProperty(SEARCH_HOST);
- }
-
- @Test
- public void accept_throws_MessageException_if_searchHost_is_blank() {
- settings.set(SEARCH_HOST, " ");
- checkMandatoryProperty(SEARCH_HOST);
- }
-
- @Test
- public void accept_throws_MessageException_if_clusterHosts_is_missing() {
- settings.clearProperty(CLUSTER_HOSTS);
- checkMandatoryProperty(CLUSTER_HOSTS);
- }
-
- @Test
- public void accept_throws_MessageException_if_clusterHosts_is_blank() {
- settings.set(CLUSTER_HOSTS, " ");
- checkMandatoryProperty(CLUSTER_HOSTS);
- }
-
- @Test
- public void accept_throws_MessageException_if_clusterSearchHosts_is_missing() {
- settings.clearProperty(CLUSTER_SEARCH_HOSTS);
- checkMandatoryProperty(CLUSTER_SEARCH_HOSTS);
- }
-
- @Test
- public void accept_throws_MessageException_if_clusterSearchHosts_is_blank() {
- settings.set(CLUSTER_SEARCH_HOSTS, " ");
- checkMandatoryProperty(CLUSTER_SEARCH_HOSTS);
- }
-
- private void checkMandatoryProperty(String key) {
- expectedException.expect(MessageException.class);
- expectedException.expectMessage(format("Property [%s] is mandatory", key));
-
- new ClusterSettings().accept(settings.getProps());
- }
-
- private static TestAppSettings getClusterSettings() {
- TestAppSettings testAppSettings = new TestAppSettings()
- .set(CLUSTER_ENABLED, "true")
- .set(CLUSTER_SEARCH_HOSTS, "localhost")
- .set(CLUSTER_HOSTS, "192.168.233.1, 192.168.233.2,192.168.233.3")
- .set(SEARCH_HOST, "192.168.233.1")
- .set(JDBC_URL, "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
- return testAppSettings;
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import java.util.Properties;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class CommandLineParserTest {
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- @Test
- public void parseArguments() {
- System.setProperty("CommandLineParserTest.unused", "unused");
- System.setProperty("sonar.CommandLineParserTest.used", "used");
-
- Properties p = CommandLineParser.parseArguments(new String[] {"-Dsonar.foo=bar"});
-
- // test environment can already declare some system properties prefixed by "sonar."
- // so we can't test the exact number "2"
- assertThat(p.size()).isGreaterThanOrEqualTo(2);
- assertThat(p.getProperty("sonar.foo")).isEqualTo("bar");
- assertThat(p.getProperty("sonar.CommandLineParserTest.used")).isEqualTo("used");
-
- }
-
- @Test
- public void argumentsToProperties_throws_IAE_if_argument_does_not_start_with_minusD() {
- Properties p = CommandLineParser.argumentsToProperties(new String[] {"-Dsonar.foo=bar", "-Dsonar.whitespace=foo bar"});
- assertThat(p).hasSize(2);
- assertThat(p.getProperty("sonar.foo")).isEqualTo("bar");
- assertThat(p.getProperty("sonar.whitespace")).isEqualTo("foo bar");
-
- expectedException.expect(IllegalArgumentException.class);
- expectedException.expectMessage("Command-line argument must start with -D, for example -Dsonar.jdbc.username=sonar. Got: sonar.bad=true");
-
- CommandLineParser.argumentsToProperties(new String[] {"-Dsonar.foo=bar", "sonar.bad=true"});
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import java.io.File;
-import java.util.Properties;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-import org.sonar.process.Props;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.process.ProcessProperties.PATH_DATA;
-import static org.sonar.process.ProcessProperties.PATH_HOME;
-import static org.sonar.process.ProcessProperties.PATH_LOGS;
-import static org.sonar.process.ProcessProperties.PATH_TEMP;
-import static org.sonar.process.ProcessProperties.PATH_WEB;
-
-
-public class FileSystemSettingsTest {
-
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- private FileSystemSettings underTest = new FileSystemSettings();
- private File homeDir;
-
- @Before
- public void setUp() throws Exception {
- homeDir = temp.newFolder();
- }
-
- @Test
- public void relative_paths_are_converted_to_absolute_paths() throws Exception {
- Props props = new Props(new Properties());
- props.set(PATH_HOME, homeDir.getAbsolutePath());
-
- // relative paths
- props.set(PATH_DATA, "data");
- props.set(PATH_LOGS, "logs");
- props.set(PATH_TEMP, "temp");
-
- // already absolute paths
- props.set(PATH_WEB, new File(homeDir, "web").getAbsolutePath());
-
- underTest.accept(props);
-
- assertThat(props.nonNullValue(PATH_DATA)).isEqualTo(new File(homeDir, "data").getAbsolutePath());
- assertThat(props.nonNullValue(PATH_LOGS)).isEqualTo(new File(homeDir, "logs").getAbsolutePath());
- assertThat(props.nonNullValue(PATH_TEMP)).isEqualTo(new File(homeDir, "temp").getAbsolutePath());
- assertThat(props.nonNullValue(PATH_WEB)).isEqualTo(new File(homeDir, "web").getAbsolutePath());
- }
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.config;
-
-import java.io.File;
-import java.net.InetAddress;
-import java.util.Properties;
-import org.apache.commons.io.FileUtils;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-import org.sonar.process.MessageException;
-import org.sonar.process.ProcessProperties;
-import org.sonar.process.Props;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.application.config.JdbcSettings.Provider;
-import static org.sonar.process.ProcessProperties.JDBC_URL;
-
-public class JdbcSettingsTest {
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
-
- private JdbcSettings underTest = new JdbcSettings();
- private File homeDir;
-
- @Before
- public void setUp() throws Exception {
- homeDir = temp.newFolder();
- }
-
- @Test
- public void resolve_H2_provider_when_props_is_empty_and_set_URL_to_default_H2() {
- Props props = newProps();
-
- assertThat(underTest.resolveProviderAndEnforceNonnullJdbcUrl(props))
- .isEqualTo(Provider.H2);
- assertThat(props.nonNullValue(JDBC_URL)).isEqualTo(String.format("jdbc:h2:tcp://%s:9092/sonar", InetAddress.getLoopbackAddress().getHostAddress()));
- }
-
- @Test
- public void resolve_Oracle_when_jdbc_url_contains_oracle_in_any_case() {
- checkProviderForUrlAndUnchangedUrl("jdbc:oracle:foo", Provider.ORACLE);
- checkProviderForUrlAndUnchangedUrl("jdbc:OrAcLe:foo", Provider.ORACLE);
- }
-
- @Test
- public void resolve_MySql_when_jdbc_url_contains_mysql_in_any_case() {
- checkProviderForUrlAndUnchangedUrl("jdbc:mysql:foo", Provider.MYSQL);
-
- checkProviderForUrlAndUnchangedUrl("jdbc:mYsQL:foo", Provider.MYSQL);
- }
-
- @Test
- public void resolve_SqlServer_when_jdbc_url_contains_sqlserver_in_any_case() {
- checkProviderForUrlAndUnchangedUrl("jdbc:sqlserver:foo", Provider.SQLSERVER);
-
- checkProviderForUrlAndUnchangedUrl("jdbc:SQLSeRVeR:foo", Provider.SQLSERVER);
- }
-
- @Test
- public void resolve_POSTGRESQL_when_jdbc_url_contains_POSTGRESQL_in_any_case() {
- checkProviderForUrlAndUnchangedUrl("jdbc:postgresql:foo", Provider.POSTGRESQL);
-
- checkProviderForUrlAndUnchangedUrl("jdbc:POSTGRESQL:foo", Provider.POSTGRESQL);
- }
-
- private void checkProviderForUrlAndUnchangedUrl(String url, Provider expected) {
- Props props = newProps(JDBC_URL, url);
-
- assertThat(underTest.resolveProviderAndEnforceNonnullJdbcUrl(props)).isEqualTo(expected);
- assertThat(props.nonNullValue(JDBC_URL)).isEqualTo(url);
- }
-
- @Test
- public void fail_with_MessageException_when_provider_is_not_supported() {
- Props props = newProps(JDBC_URL, "jdbc:microsoft:sqlserver://localhost");
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Unsupported JDBC driver provider: microsoft");
-
- underTest.resolveProviderAndEnforceNonnullJdbcUrl(props);
- }
-
- @Test
- public void fail_with_MessageException_when_url_does_not_have_jdbc_prefix() {
- Props props = newProps(JDBC_URL, "oracle:thin:@localhost/XE");
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Bad format of JDBC URL: oracle:thin:@localhost/XE");
-
- underTest.resolveProviderAndEnforceNonnullJdbcUrl(props);
- }
-
- @Test
- public void check_mysql_parameters() {
- // minimal -> ok
- underTest.checkUrlParameters(Provider.MYSQL,
- "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8");
-
- // full -> ok
- underTest.checkUrlParameters(Provider.MYSQL,
- "jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance");
-
- // missing required -> ko
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("JDBC URL must have the property 'useUnicode=true'");
-
- underTest.checkUrlParameters(Provider.MYSQL, "jdbc:mysql://localhost:3306/sonar?characterEncoding=utf8");
- }
-
- @Test
- public void checkAndComplete_sets_driver_path_for_oracle() throws Exception {
- File driverFile = new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc6.jar");
- FileUtils.touch(driverFile);
-
- Props props = newProps(JDBC_URL, "jdbc:oracle:thin:@localhost/XE");
- underTest.accept(props);
- assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
- }
-
- @Test
- public void sets_driver_path_for_h2() throws Exception {
- File driverFile = new File(homeDir, "lib/jdbc/h2/h2.jar");
- FileUtils.touch(driverFile);
-
- Props props = newProps(JDBC_URL, "jdbc:h2:tcp://localhost:9092/sonar");
- underTest.accept(props);
- assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
- }
-
- @Test
- public void checkAndComplete_sets_driver_path_for_postgresql() throws Exception {
- File driverFile = new File(homeDir, "lib/jdbc/postgresql/pg.jar");
- FileUtils.touch(driverFile);
-
- Props props = newProps(JDBC_URL, "jdbc:postgresql://localhost/sonar");
- underTest.accept(props);
- assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
- }
-
- @Test
- public void checkAndComplete_sets_driver_path_for_mssql() throws Exception {
- File driverFile = new File(homeDir, "lib/jdbc/mssql/sqljdbc4.jar");
- FileUtils.touch(driverFile);
-
- Props props = newProps(JDBC_URL, "jdbc:sqlserver://localhost/sonar;SelectMethod=Cursor");
- underTest.accept(props);
- assertThat(props.nonNullValueAsFile(ProcessProperties.JDBC_DRIVER_PATH)).isEqualTo(driverFile);
- }
-
- @Test
- public void driver_file() throws Exception {
- File driverFile = new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc6.jar");
- FileUtils.touch(driverFile);
-
- String path = underTest.driverPath(homeDir, Provider.ORACLE);
- assertThat(path).isEqualTo(driverFile.getAbsolutePath());
- }
-
- @Test
- public void driver_dir_does_not_exist() throws Exception {
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Directory does not exist: extensions/jdbc-driver/oracle");
-
- underTest.driverPath(homeDir, Provider.ORACLE);
- }
-
- @Test
- public void no_files_in_driver_dir() throws Exception {
- FileUtils.forceMkdir(new File(homeDir, "extensions/jdbc-driver/oracle"));
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Directory does not contain JDBC driver: extensions/jdbc-driver/oracle");
-
- underTest.driverPath(homeDir, Provider.ORACLE);
- }
-
- @Test
- public void too_many_files_in_driver_dir() throws Exception {
- FileUtils.touch(new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc5.jar"));
- FileUtils.touch(new File(homeDir, "extensions/jdbc-driver/oracle/ojdbc6.jar"));
-
- expectedException.expect(MessageException.class);
- expectedException.expectMessage("Directory must contain only one JAR file: extensions/jdbc-driver/oracle");
-
- underTest.driverPath(homeDir, Provider.ORACLE);
- }
-
- private Props newProps(String... params) {
- Properties properties = new Properties();
- for (int i = 0; i < params.length; i++) {
- properties.setProperty(params[i], params[i + 1]);
- i++;
- }
- properties.setProperty(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
- return new Props(properties);
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.config;
-
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class SonarQubeVersionHelperTest {
- @Test
- public void getSonarQubeVersion_must_not_return_an_empty_string() {
- assertThat(SonarQubeVersionHelper.getSonarqubeVersion()).isNotEmpty();
- }
-
- @Test
- public void getSonarQubeVersion_must_always_return_same_value() {
- String sonarqubeVersion = SonarQubeVersionHelper.getSonarqubeVersion();
- for (int i = 0; i < 3; i++) {
- assertThat(SonarQubeVersionHelper.getSonarqubeVersion()).isEqualTo(sonarqubeVersion);
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
-package org.sonar.application.config;
-
-import java.util.Optional;
-import java.util.Properties;
-import org.sonar.process.ProcessProperties;
-import org.sonar.process.Props;
-
-/**
- * Simple implementation of {@link AppSettings} that loads
- * the default values defined by {@link ProcessProperties}.
- */
-public class TestAppSettings implements AppSettings {
-
- private Props props;
-
- public TestAppSettings() {
- this.props = new Props(new Properties());
- ProcessProperties.completeDefaults(this.props);
- }
-
- public TestAppSettings set(String key, String value) {
- this.props.set(key, value);
- return this;
- }
-
- @Override
- public Props getProps() {
- return props;
- }
-
- @Override
- public Optional<String> getValue(String key) {
- return Optional.ofNullable(props.value(key));
- }
-
- @Override
- public void reload(Props copy) {
- this.props = copy;
- }
-
- public void clearProperty(String key) {
- this.props.rawProperties().remove(key);
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import org.junit.Test;
-import org.sonar.application.process.Lifecycle;
-import org.sonar.application.process.ProcessLifecycleListener;
-import org.sonar.process.ProcessId;
-
-import static java.util.Arrays.asList;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.sonar.application.process.Lifecycle.State.INIT;
-import static org.sonar.application.process.Lifecycle.State.STARTED;
-import static org.sonar.application.process.Lifecycle.State.STARTING;
-import static org.sonar.application.process.Lifecycle.State.STOPPING;
-
-public class LifecycleTest {
-
- @Test
- public void initial_state_is_INIT() {
- Lifecycle lifecycle = new Lifecycle(ProcessId.ELASTICSEARCH, Collections.emptyList());
- assertThat(lifecycle.getState()).isEqualTo(INIT);
- }
-
- @Test
- public void try_to_move_does_not_support_jumping_states() {
- TestLifeCycleListener listener = new TestLifeCycleListener();
- Lifecycle lifecycle = new Lifecycle(ProcessId.ELASTICSEARCH, asList(listener));
- assertThat(lifecycle.getState()).isEqualTo(INIT);
- assertThat(listener.states).isEmpty();
-
- assertThat(lifecycle.tryToMoveTo(STARTED)).isFalse();
- assertThat(lifecycle.getState()).isEqualTo(INIT);
- assertThat(listener.states).isEmpty();
-
- assertThat(lifecycle.tryToMoveTo(STARTING)).isTrue();
- assertThat(lifecycle.getState()).isEqualTo(STARTING);
- assertThat(listener.states).containsOnly(STARTING);
- }
-
- @Test
- public void no_state_can_not_move_to_itself() {
- for (Lifecycle.State state : Lifecycle.State.values()) {
- assertThat(newLifeCycle(state).tryToMoveTo(state)).isFalse();
- }
- }
-
- @Test
- public void can_move_to_STOPPING_from_STARTING_STARTED_only() {
- for (Lifecycle.State state : Lifecycle.State.values()) {
- TestLifeCycleListener listener = new TestLifeCycleListener();
- boolean tryToMoveTo = newLifeCycle(state, listener).tryToMoveTo(STOPPING);
- if (state == STARTING || state == STARTED) {
- assertThat(tryToMoveTo).as("from state " + state).isTrue();
- assertThat(listener.states).containsOnly(STOPPING);
- } else {
- assertThat(tryToMoveTo).as("from state " + state).isFalse();
- assertThat(listener.states).isEmpty();
- }
- }
- }
-
- @Test
- public void can_move_to_STARTED_from_STARTING_only() {
- for (Lifecycle.State state : Lifecycle.State.values()) {
- TestLifeCycleListener listener = new TestLifeCycleListener();
- boolean tryToMoveTo = newLifeCycle(state, listener).tryToMoveTo(STARTED);
- if (state == STARTING) {
- assertThat(tryToMoveTo).as("from state " + state).isTrue();
- assertThat(listener.states).containsOnly(STARTED);
- } else {
- assertThat(tryToMoveTo).as("from state " + state).isFalse();
- assertThat(listener.states).isEmpty();
- }
- }
- }
-
- private static Lifecycle newLifeCycle(Lifecycle.State state, TestLifeCycleListener... listeners) {
- return new Lifecycle(ProcessId.ELASTICSEARCH, Arrays.asList(listeners), state);
- }
-
- private static final class TestLifeCycleListener implements ProcessLifecycleListener {
- private final List<Lifecycle.State> states = new ArrayList<>();
-
- @Override
- public void onProcessState(ProcessId processId, Lifecycle.State state) {
- this.states.add(state);
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.concurrent.TimeUnit;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.sonar.process.sharedmemoryfile.ProcessCommands;
-import org.sonar.process.ProcessId;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
-import static org.mockito.Mockito.doThrow;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-
-public class ProcessCommandsProcessMonitorTest {
-
- @Test
- public void ProcessMonitorImpl_is_a_proxy_of_Process() throws Exception {
- Process process = mock(Process.class, RETURNS_DEEP_STUBS);
- ProcessCommands commands = mock(ProcessCommands.class, RETURNS_DEEP_STUBS);
-
- ProcessCommandsProcessMonitor underTest = new ProcessCommandsProcessMonitor(process, ProcessId.WEB_SERVER, commands);
-
- underTest.waitFor();
- verify(process).waitFor();
-
- underTest.closeStreams();
- verify(process.getErrorStream()).close();
- verify(process.getInputStream()).close();
- verify(process.getOutputStream()).close();
-
- underTest.destroyForcibly();
- verify(process).destroyForcibly();
-
- assertThat(underTest.getInputStream()).isNotNull();
-
- underTest.isAlive();
- verify(process).isAlive();
-
- underTest.waitFor(123, TimeUnit.MILLISECONDS);
- verify(process).waitFor(123, TimeUnit.MILLISECONDS);
- }
-
- @Test
- public void ProcessMonitorImpl_is_a_proxy_of_Commands() throws Exception {
- Process process = mock(Process.class, RETURNS_DEEP_STUBS);
- ProcessCommands commands = mock(ProcessCommands.class, RETURNS_DEEP_STUBS);
-
- ProcessCommandsProcessMonitor underTest = new ProcessCommandsProcessMonitor(process, null, commands);
-
- underTest.askForStop();
- verify(commands).askForStop();
-
- underTest.acknowledgeAskForRestart();
- verify(commands).acknowledgeAskForRestart();
-
- underTest.askedForRestart();
- verify(commands).askedForRestart();
-
- underTest.isOperational();
- verify(commands).isOperational();
- }
-
- @Test
- public void closeStreams_ignores_null_stream() {
- ProcessCommands commands = mock(ProcessCommands.class);
- Process process = mock(Process.class);
- when(process.getInputStream()).thenReturn(null);
-
- ProcessCommandsProcessMonitor underTest = new ProcessCommandsProcessMonitor(process, null, commands);
-
- // no failures
- underTest.closeStreams();
- }
-
- @Test
- public void closeStreams_ignores_failure_if_stream_fails_to_be_closed() throws Exception {
- InputStream stream = mock(InputStream.class);
- doThrow(new IOException("error")).when(stream).close();
- Process process = mock(Process.class);
- when(process.getInputStream()).thenReturn(stream);
-
- ProcessCommandsProcessMonitor underTest = new ProcessCommandsProcessMonitor(process, null, mock(ProcessCommands.class, Mockito.RETURNS_MOCKS));
-
- // no failures
- underTest.closeStreams();
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.io.File;
-import java.io.FileReader;
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TemporaryFolder;
-import org.sonar.process.ProcessId;
-import org.sonar.process.command.JavaCommand;
-import org.sonar.process.jmvoptions.JvmOptions;
-import org.sonar.process.sharedmemoryfile.AllProcessesCommands;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.data.MapEntry.entry;
-import static org.mockito.Mockito.RETURNS_MOCKS;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public class ProcessLauncherImplTest {
-
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
-
- private AllProcessesCommands commands = mock(AllProcessesCommands.class, RETURNS_MOCKS);
-
- @Test
- public void launch_forks_a_new_process() throws Exception {
- File tempDir = temp.newFolder();
- TestProcessBuilder processBuilder = new TestProcessBuilder();
- ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder);
- JavaCommand<JvmOptions> command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder());
- command.addClasspath("lib/*.class");
- command.addClasspath("lib/*.jar");
- command.setArgument("foo", "bar");
- command.setClassName("org.sonarqube.Main");
- command.setEnvVariable("VAR1", "valueOfVar1");
- command.setJvmOptions(new JvmOptions<>()
- .add("-Dfoo=bar")
- .add("-Dfoo2=bar2"));
-
- ProcessMonitor monitor = underTest.launch(command);
-
- assertThat(monitor).isNotNull();
- assertThat(processBuilder.started).isTrue();
- assertThat(processBuilder.commands.get(0)).endsWith("java");
- assertThat(processBuilder.commands).containsSequence(
- "-Dfoo=bar",
- "-Dfoo2=bar2",
- "-cp",
- "lib/*.class" + System.getProperty("path.separator") + "lib/*.jar",
- "org.sonarqube.Main");
- assertThat(processBuilder.dir).isEqualTo(command.getWorkDir());
- assertThat(processBuilder.redirectErrorStream).isTrue();
- assertThat(processBuilder.environment)
- .contains(entry("VAR1", "valueOfVar1"))
- .containsAllEntriesOf(command.getEnvVariables());
- }
-
- @Test
- public void properties_are_passed_to_command_via_a_temporary_properties_file() throws Exception {
- File tempDir = temp.newFolder();
- TestProcessBuilder processBuilder = new TestProcessBuilder();
- ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder);
- JavaCommand<JvmOptions> command = new JavaCommand<>(ProcessId.ELASTICSEARCH, temp.newFolder());
- command.setArgument("foo", "bar");
- command.setArgument("baz", "woo");
- command.setJvmOptions(new JvmOptions<>());
-
- underTest.launch(command);
-
- String propsFilePath = processBuilder.commands.get(processBuilder.commands.size() - 1);
- File file = new File(propsFilePath);
- assertThat(file).exists().isFile();
- try (FileReader reader = new FileReader(file)) {
- Properties props = new Properties();
- props.load(reader);
- assertThat(props).containsOnly(
- entry("foo", "bar"),
- entry("baz", "woo"),
- entry("process.terminationTimeout", "60000"),
- entry("process.key", ProcessId.ELASTICSEARCH.getKey()),
- entry("process.index", String.valueOf(ProcessId.ELASTICSEARCH.getIpcIndex())),
- entry("process.sharedDir", tempDir.getAbsolutePath()));
- }
- }
-
- @Test
- public void throw_ISE_if_command_fails() throws IOException {
- File tempDir = temp.newFolder();
- ProcessLauncherImpl.ProcessBuilder processBuilder = mock(ProcessLauncherImpl.ProcessBuilder.class, RETURNS_MOCKS);
- when(processBuilder.start()).thenThrow(new IOException("error"));
- ProcessLauncher underTest = new ProcessLauncherImpl(tempDir, commands, () -> processBuilder);
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("Fail to launch process [es]");
-
- underTest.launch(new JavaCommand(ProcessId.ELASTICSEARCH, temp.newFolder()));
- }
-
- private static class TestProcessBuilder implements ProcessLauncherImpl.ProcessBuilder {
- private List<String> commands = null;
- private File dir = null;
- private Boolean redirectErrorStream = null;
- private final Map<String, String> environment = new HashMap<>();
- private boolean started = false;
-
- @Override
- public List<String> command() {
- return commands;
- }
-
- @Override
- public TestProcessBuilder command(List<String> commands) {
- this.commands = commands;
- return this;
- }
-
- @Override
- public TestProcessBuilder directory(File dir) {
- this.dir = dir;
- return this;
- }
-
- @Override
- public Map<String, String> environment() {
- return environment;
- }
-
- @Override
- public TestProcessBuilder redirectErrorStream(boolean b) {
- this.redirectErrorStream = b;
- return this;
- }
-
- @Override
- public Process start() throws IOException {
- this.started = true;
- return mock(Process.class);
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.io.InputStream;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.DisableOnDebug;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.TestRule;
-import org.junit.rules.Timeout;
-import org.mockito.Mockito;
-import org.sonar.process.ProcessId;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-
-public class SQProcessTest {
-
- private static final ProcessId A_PROCESS_ID = ProcessId.ELASTICSEARCH;
-
- @Rule
- public ExpectedException expectedException = ExpectedException.none();
- @Rule
- public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
-
- @Test
- public void initial_state_is_INIT() {
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
-
- assertThat(underTest.getProcessId()).isEqualTo(A_PROCESS_ID);
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.INIT);
- }
-
- @Test
- public void start_and_stop_process() {
- ProcessLifecycleListener listener = mock(ProcessLifecycleListener.class);
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
- .addProcessLifecycleListener(listener)
- .build();
-
- try (TestProcess testProcess = new TestProcess()) {
- assertThat(underTest.start(() -> testProcess)).isTrue();
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STARTED);
- assertThat(testProcess.isAlive()).isTrue();
- assertThat(testProcess.streamsClosed).isFalse();
- verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STARTED);
-
- testProcess.close();
- // do not wait next run of watcher threads
- underTest.refreshState();
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
- assertThat(testProcess.isAlive()).isFalse();
- assertThat(testProcess.streamsClosed).isTrue();
- verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPED);
- }
- }
-
- @Test
- public void start_does_not_nothing_if_already_started_once() {
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
-
- try (TestProcess testProcess = new TestProcess()) {
- assertThat(underTest.start(() -> testProcess)).isTrue();
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STARTED);
-
- assertThat(underTest.start(() -> {throw new IllegalStateException();})).isFalse();
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STARTED);
- }
- }
-
- @Test
- public void start_throws_exception_and_move_to_state_STOPPED_if_execution_of_command_fails() {
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
-
- expectedException.expect(IllegalStateException.class);
- expectedException.expectMessage("error");
-
- underTest.start(() -> {throw new IllegalStateException("error");});
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
- }
-
- @Test
- public void send_event_when_process_is_operational() {
- ProcessEventListener listener = mock(ProcessEventListener.class);
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
- .addEventListener(listener)
- .build();
-
- try (TestProcess testProcess = new TestProcess()) {
- underTest.start(() -> testProcess);
-
- testProcess.operational = true;
- underTest.refreshState();
-
- verify(listener).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.OPERATIONAL);
- }
- verifyNoMoreInteractions(listener);
- }
-
- @Test
- public void operational_event_is_sent_once() {
- ProcessEventListener listener = mock(ProcessEventListener.class);
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
- .addEventListener(listener)
- .build();
-
- try (TestProcess testProcess = new TestProcess()) {
- underTest.start(() -> testProcess);
- testProcess.operational = true;
-
- underTest.refreshState();
- verify(listener).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.OPERATIONAL);
-
- // second run
- underTest.refreshState();
- verifyNoMoreInteractions(listener);
- }
- }
-
- @Test
- public void send_event_when_process_requests_for_restart() {
- ProcessEventListener listener = mock(ProcessEventListener.class);
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
- .addEventListener(listener)
- .setWatcherDelayMs(1L)
- .build();
-
- try (TestProcess testProcess = new TestProcess()) {
- underTest.start(() -> testProcess);
-
- testProcess.askedForRestart = true;
- verify(listener, timeout(10_000)).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.ASK_FOR_RESTART);
-
- // flag is reset so that next run does not trigger again the event
- underTest.refreshState();
- verifyNoMoreInteractions(listener);
- assertThat(testProcess.askedForRestart).isFalse();
- }
- }
-
- @Test
- public void stopForcibly_stops_the_process_without_graceful_request_for_stop() {
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
-
- try (TestProcess testProcess = new TestProcess()) {
- underTest.start(() -> testProcess);
-
- underTest.stopForcibly();
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
- assertThat(testProcess.askedForStop).isFalse();
- assertThat(testProcess.destroyedForcibly).isTrue();
-
- // second execution of stopForcibly does nothing. It's still stopped.
- underTest.stopForcibly();
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
- }
- }
-
- @Test
- public void process_stops_after_graceful_request_for_stop() throws Exception {
- ProcessLifecycleListener listener = mock(ProcessLifecycleListener.class);
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
- .addProcessLifecycleListener(listener)
- .build();
-
- try (TestProcess testProcess = new TestProcess()) {
- underTest.start(() -> testProcess);
-
- Thread stopperThread = new Thread(() -> underTest.stop(1, TimeUnit.HOURS));
- stopperThread.start();
-
- // thread is blocked until process stopped
- assertThat(stopperThread.isAlive()).isTrue();
-
- // wait for the stopper thread to ask graceful stop
- while (!testProcess.askedForStop) {
- Thread.sleep(1L);
- }
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPING);
- verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPING);
-
- // process stopped
- testProcess.close();
-
- // waiting for stopper thread to detect and handle the stop
- stopperThread.join();
-
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
- verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPED);
- }
- }
-
- @Test
- public void process_is_stopped_forcibly_if_graceful_stop_is_too_long() throws Exception {
- ProcessLifecycleListener listener = mock(ProcessLifecycleListener.class);
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
- .addProcessLifecycleListener(listener)
- .build();
-
- try (TestProcess testProcess = new TestProcess()) {
- underTest.start(() -> testProcess);
-
- underTest.stop(1L, TimeUnit.MILLISECONDS);
-
- testProcess.waitFor();
- assertThat(testProcess.askedForStop).isTrue();
- assertThat(testProcess.destroyedForcibly).isTrue();
- assertThat(testProcess.isAlive()).isFalse();
- assertThat(underTest.getState()).isEqualTo(Lifecycle.State.STOPPED);
- verify(listener).onProcessState(A_PROCESS_ID, Lifecycle.State.STOPPED);
- }
- }
-
- @Test
- public void process_requests_are_listened_on_regular_basis() throws Exception {
- ProcessEventListener listener = mock(ProcessEventListener.class);
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID)
- .addEventListener(listener)
- .setWatcherDelayMs(1L)
- .build();
-
- try (TestProcess testProcess = new TestProcess()) {
- underTest.start(() -> testProcess);
-
- testProcess.operational = true;
-
- verify(listener, timeout(1_000L)).onProcessEvent(A_PROCESS_ID, ProcessEventListener.Type.OPERATIONAL);
- }
- }
-
- @Test
- public void test_toString() {
- SQProcess underTest = SQProcess.builder(A_PROCESS_ID).build();
- assertThat(underTest.toString()).isEqualTo("Process[" + A_PROCESS_ID.getKey() + "]");
- }
-
- private static class TestProcess implements ProcessMonitor, AutoCloseable {
-
- private final CountDownLatch alive = new CountDownLatch(1);
- private final InputStream inputStream = mock(InputStream.class, Mockito.RETURNS_MOCKS);
- private final InputStream errorStream = mock(InputStream.class, Mockito.RETURNS_MOCKS);
- private boolean streamsClosed = false;
- private boolean operational = false;
- private boolean askedForRestart = false;
- private boolean askedForStop = false;
- private boolean destroyedForcibly = false;
-
- @Override
- public InputStream getInputStream() {
- return inputStream;
- }
-
- @Override
- public InputStream getErrorStream() {
- return errorStream;
- }
-
- @Override
- public void closeStreams() {
- streamsClosed = true;
- }
-
- @Override
- public boolean isAlive() {
- return alive.getCount() == 1;
- }
-
- @Override
- public void askForStop() {
- askedForStop = true;
- // do not stop, just asking
- }
-
- @Override
- public void destroyForcibly() {
- destroyedForcibly = true;
- alive.countDown();
- }
-
- @Override
- public void waitFor() throws InterruptedException {
- alive.await();
- }
-
- @Override
- public void waitFor(long timeout, TimeUnit timeoutUnit) throws InterruptedException {
- alive.await(timeout, timeoutUnit);
- }
-
- @Override
- public boolean isOperational() {
- return operational;
- }
-
- @Override
- public boolean askedForRestart() {
- return askedForRestart;
- }
-
- @Override
- public void acknowledgeAskForRestart() {
- this.askedForRestart = false;
- }
-
- @Override
- public void close() {
- alive.countDown();
- }
- }
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.io.IOException;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.DisableOnDebug;
-import org.junit.rules.TemporaryFolder;
-import org.junit.rules.TestRule;
-import org.junit.rules.Timeout;
-import org.sonar.application.FileSystem;
-import org.sonar.application.Scheduler;
-import org.sonar.application.config.AppSettings;
-import org.sonar.process.sharedmemoryfile.ProcessCommands;
-import org.sonar.process.ProcessProperties;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.RETURNS_DEEP_STUBS;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.timeout;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyZeroInteractions;
-import static org.mockito.Mockito.when;
-
-public class StopRequestWatcherImplTest {
-
- @Rule
- public TemporaryFolder temp = new TemporaryFolder();
- @Rule
- public TestRule safeguardTimeout = new DisableOnDebug(Timeout.seconds(60));
-
- private AppSettings settings = mock(AppSettings.class, RETURNS_DEEP_STUBS);
- private ProcessCommands commands = mock(ProcessCommands.class);
- private Scheduler scheduler = mock(Scheduler.class);
-
- @Test
- public void do_not_watch_command_if_disabled() throws IOException {
- enableSetting(false);
- StopRequestWatcherImpl underTest = new StopRequestWatcherImpl(settings, scheduler, commands);
-
- underTest.startWatching();
- assertThat(underTest.isAlive()).isFalse();
-
- underTest.stopWatching();
- verifyZeroInteractions(commands, scheduler);
- }
-
- @Test
- public void watch_stop_command_if_enabled() throws Exception {
- enableSetting(true);
- StopRequestWatcherImpl underTest = new StopRequestWatcherImpl(settings, scheduler, commands);
- underTest.setDelayMs(1L);
-
- underTest.startWatching();
- assertThat(underTest.isAlive()).isTrue();
- verify(scheduler, never()).terminate();
-
- when(commands.askedForStop()).thenReturn(true);
- verify(scheduler, timeout(1_000L)).terminate();
-
- underTest.stopWatching();
- while (underTest.isAlive()) {
- Thread.sleep(1L);
- }
- }
-
- @Test
- public void create_instance_with_default_delay() throws IOException {
- FileSystem fs = mock(FileSystem.class);
- when(fs.getTempDir()).thenReturn(temp.newFolder());
-
- StopRequestWatcherImpl underTest = StopRequestWatcherImpl.create(settings, scheduler, fs);
-
- assertThat(underTest.getDelayMs()).isEqualTo(500L);
- }
-
- @Test
- public void stop_watching_commands_if_thread_is_interrupted() throws Exception {
- enableSetting(true);
- StopRequestWatcherImpl underTest = new StopRequestWatcherImpl(settings, scheduler, commands);
-
- underTest.startWatching();
- underTest.interrupt();
-
- while (underTest.isAlive()) {
- Thread.sleep(1L);
- }
- assertThat(underTest.isAlive()).isFalse();
- }
-
- private void enableSetting(boolean b) {
- when(settings.getProps().valueAsBoolean(ProcessProperties.ENABLE_STOP_COMMAND)).thenReturn(b);
- }
-
-}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2017 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-package org.sonar.application.process;
-
-import java.io.InputStream;
-import org.apache.commons.io.IOUtils;
-import org.junit.Test;
-import org.slf4j.Logger;
-import org.sonar.application.process.StreamGobbler;
-
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.verifyNoMoreInteractions;
-import static org.mockito.Mockito.verifyZeroInteractions;
-
-public class StreamGobblerTest {
-
- @Test
- public void forward_stream_to_log() {
- InputStream stream = IOUtils.toInputStream("one\nsecond log\nthird log\n");
- Logger logger = mock(Logger.class);
-
- StreamGobbler gobbler = new StreamGobbler(stream, "WEB", logger);
- verifyZeroInteractions(logger);
-
- gobbler.start();
- StreamGobbler.waitUntilFinish(gobbler);
-
- verify(logger).info("one");
- verify(logger).info("second log");
- verify(logger).info("third log");
- verifyNoMoreInteractions(logger);
- }
-}
+++ /dev/null
-<?xml version="1.0" encoding="UTF-8" ?>
-<configuration debug="false">
- <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
-
- <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
- <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
- <pattern>
- %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n
- </pattern>
- </encoder>
- </appender>
-
- <root>
- <level value="INFO"/>
- <appender-ref ref="CONSOLE"/>
- </root>
-
-</configuration>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
- <artifactId>sonar-process-monitor</artifactId>
+ <artifactId>sonar-main</artifactId>
<version>${project.version}</version>
</dependency>
<!--must declare this dependency of sonar-process-monitor here, again,-->