aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-main
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@sonarsource.com>2017-09-14 16:33:26 +0200
committerSimon Brandhof <simon.brandhof@sonarsource.com>2017-09-26 23:49:37 +0200
commit87ce833df2754dc1a23d29e56571ba826978b7bd (patch)
tree31c73987010443b47bdff75f5aeed1d05370d094 /server/sonar-main
parente3f8991bf2bb425f2829a4767a2d5fe6e3236c8c (diff)
downloadsonarqube-87ce833df2754dc1a23d29e56571ba826978b7bd.tar.gz
sonarqube-87ce833df2754dc1a23d29e56571ba826978b7bd.zip
SONAR-9803 restrict sonar-process to classes shared by all processes only
Diffstat (limited to 'server/sonar-main')
-rw-r--r--server/sonar-main/pom.xml11
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java6
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/AbstractCommand.java104
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/CeJvmOptions.java38
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/CommandFactory.java30
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/CommandFactoryImpl.java166
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/EsCommand.java118
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/EsJvmOptions.java71
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/JavaCommand.java78
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/JvmOptions.java182
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/WebJvmOptions.java38
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/command/package-info.java23
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/es/EsFileSystem.java105
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/es/EsLogging.java50
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java148
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/es/EsYmlSettings.java56
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/es/package-info.java23
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/process/EsProcessMonitor.java2
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncher.java4
-rw-r--r--server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java10
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java8
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/command/AbstractCommandTest.java148
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/command/CeJvmOptionsTest.java42
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/command/CommandFactoryImplTest.java239
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/command/EsJvmOptionsTest.java108
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/command/JavaCommandTest.java64
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/command/JvmOptionsTest.java427
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/command/WebJvmOptionsTest.java43
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/es/EsFileSystemTest.java187
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/es/EsLoggingTest.java131
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java302
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/es/EsYmlSettingsTest.java62
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/logging/ListAppender.java53
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/process/EsProcessMonitorTest.java2
-rw-r--r--server/sonar-main/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java4
35 files changed, 3065 insertions, 18 deletions
diff --git a/server/sonar-main/pom.xml b/server/sonar-main/pom.xml
index 9aa79852803..707e0fb6016 100644
--- a/server/sonar-main/pom.xml
+++ b/server/sonar-main/pom.xml
@@ -90,6 +90,17 @@
<artifactId>hazelcast-client</artifactId>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>${project.groupId}</groupId>
+ <artifactId>sonar-testing-harness</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.tngtech.java</groupId>
+ <artifactId>junit-dataprovider</artifactId>
+ <scope>test</scope>
+ </dependency>
+
</dependencies>
<build>
diff --git a/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java b/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java
index 3268bb249c6..0f9214e20b1 100644
--- a/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java
+++ b/server/sonar-main/src/main/java/org/sonar/application/SchedulerImpl.java
@@ -43,9 +43,9 @@ import org.sonar.application.process.ProcessLifecycleListener;
import org.sonar.application.process.ProcessMonitor;
import org.sonar.application.process.SQProcess;
import org.sonar.process.ProcessId;
-import org.sonar.process.command.CommandFactory;
-import org.sonar.process.command.EsCommand;
-import org.sonar.process.command.JavaCommand;
+import org.sonar.application.command.CommandFactory;
+import org.sonar.application.command.EsCommand;
+import org.sonar.application.command.JavaCommand;
public class SchedulerImpl implements Scheduler, ProcessEventListener, ProcessLifecycleListener, AppStateListener {
diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/AbstractCommand.java b/server/sonar-main/src/main/java/org/sonar/application/command/AbstractCommand.java
new file mode 100644
index 00000000000..2da97bb8f8f
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/command/AbstractCommand.java
@@ -0,0 +1,104 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+import javax.annotation.Nullable;
+import org.sonar.process.ProcessId;
+import org.sonar.process.System2;
+
+import static java.util.Objects.requireNonNull;
+
+public abstract class AbstractCommand<T extends AbstractCommand> {
+ // unique key among the group of commands to launch
+ private final ProcessId id;
+ // program arguments
+ private final Map<String, String> arguments = new LinkedHashMap<>();
+ private final Map<String, String> envVariables;
+ private final Set<String> suppressedEnvVariables = new HashSet<>();
+ private final File workDir;
+
+ protected AbstractCommand(ProcessId id, File workDir, System2 system2) {
+ this.id = requireNonNull(id, "ProcessId can't be null");
+ this.workDir = requireNonNull(workDir, "workDir can't be null");
+ this.envVariables = new HashMap<>(system2.getenv());
+ }
+
+ public ProcessId getProcessId() {
+ return id;
+ }
+
+ public File getWorkDir() {
+ return workDir;
+ }
+
+ @SuppressWarnings("unchecked")
+ private T castThis() {
+ return (T) this;
+ }
+
+ public Map<String, String> getArguments() {
+ return arguments;
+ }
+
+ public T setArgument(String key, @Nullable String value) {
+ if (value == null) {
+ arguments.remove(key);
+ } else {
+ arguments.put(key, value);
+ }
+ return castThis();
+ }
+
+ public T setArguments(Properties args) {
+ for (Map.Entry<Object, Object> entry : args.entrySet()) {
+ setArgument(entry.getKey().toString(), entry.getValue() != null ? entry.getValue().toString() : null);
+ }
+ return castThis();
+ }
+
+ public Map<String, String> getEnvVariables() {
+ return envVariables;
+ }
+
+ public Set<String> getSuppressedEnvVariables() {
+ return suppressedEnvVariables;
+ }
+
+ public T suppressEnvVariable(String key) {
+ requireNonNull(key, "key can't be null");
+ suppressedEnvVariables.add(key);
+ envVariables.remove(key);
+ return castThis();
+ }
+
+ public T setEnvVariable(String key, String value) {
+ envVariables.put(
+ requireNonNull(key, "key can't be null"),
+ requireNonNull(value, "value can't be null"));
+ return castThis();
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/CeJvmOptions.java b/server/sonar-main/src/main/java/org/sonar/application/command/CeJvmOptions.java
new file mode 100644
index 00000000000..57e4b1b090e
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/command/CeJvmOptions.java
@@ -0,0 +1,38 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class CeJvmOptions extends JvmOptions<CeJvmOptions> {
+ public CeJvmOptions(File tmpDir) {
+ super(mandatoryOptions(tmpDir));
+ }
+
+ private static Map<String, String> mandatoryOptions(File tmpDir) {
+ Map<String, String> res = new LinkedHashMap<>(3);
+ res.put("-Djava.awt.headless=", "true");
+ res.put("-Dfile.encoding=", "UTF-8");
+ res.put("-Djava.io.tmpdir=", tmpDir.getAbsolutePath());
+ return res;
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactory.java b/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactory.java
new file mode 100644
index 00000000000..e55f364be11
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactory.java
@@ -0,0 +1,30 @@
+/*
+ * 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.command;
+
+public interface CommandFactory {
+
+ EsCommand createEsCommand();
+
+ JavaCommand createWebCommand(boolean leader);
+
+ JavaCommand createCeCommand();
+
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactoryImpl.java b/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactoryImpl.java
new file mode 100644
index 00000000000..0df586aa7f0
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/command/CommandFactoryImpl.java
@@ -0,0 +1,166 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.util.Map;
+import java.util.Optional;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+import org.sonar.process.System2;
+import org.sonar.application.es.EsFileSystem;
+import org.sonar.application.es.EsLogging;
+import org.sonar.application.es.EsSettings;
+import org.sonar.application.es.EsYmlSettings;
+
+import static org.sonar.process.ProcessProperties.HTTPS_PROXY_HOST;
+import static org.sonar.process.ProcessProperties.HTTPS_PROXY_PORT;
+import static org.sonar.process.ProcessProperties.HTTP_PROXY_HOST;
+import static org.sonar.process.ProcessProperties.HTTP_PROXY_PORT;
+
+public class CommandFactoryImpl implements CommandFactory {
+ private static final String ENV_VAR_JAVA_TOOL_OPTIONS = "JAVA_TOOL_OPTIONS";
+ /**
+ * Properties about proxy that must be set as system properties
+ */
+ private static final String[] PROXY_PROPERTY_KEYS = new String[] {
+ HTTP_PROXY_HOST,
+ HTTP_PROXY_PORT,
+ "http.nonProxyHosts",
+ HTTPS_PROXY_HOST,
+ HTTPS_PROXY_PORT,
+ "http.auth.ntlm.domain",
+ "socksProxyHost",
+ "socksProxyPort"};
+
+ private final Props props;
+ private final File tempDir;
+
+ public CommandFactoryImpl(Props props, File tempDir, System2 system2) {
+ this.props = props;
+ this.tempDir = tempDir;
+ String javaToolOptions = system2.getenv(ENV_VAR_JAVA_TOOL_OPTIONS);
+ if (javaToolOptions != null && !javaToolOptions.trim().isEmpty()) {
+ LoggerFactory.getLogger(CommandFactoryImpl.class)
+ .warn("JAVA_TOOL_OPTIONS is defined but will be ignored. " +
+ "Use properties sonar.*.javaOpts and/or sonar.*.javaAdditionalOpts in sonar.properties to change SQ JVM processes options");
+ }
+ }
+
+ @Override
+ public EsCommand createEsCommand() {
+ EsFileSystem esFileSystem = new EsFileSystem(props);
+ if (!esFileSystem.getExecutable().exists()) {
+ throw new IllegalStateException("Cannot find elasticsearch binary");
+ }
+ Map<String, String> settingsMap = new EsSettings(props, esFileSystem, System2.INSTANCE).build();
+
+ return new EsCommand(ProcessId.ELASTICSEARCH, esFileSystem.getHomeDirectory())
+ .setFileSystem(esFileSystem)
+ .setLog4j2Properties(new EsLogging().createProperties(props, esFileSystem.getLogDirectory()))
+ .setArguments(props.rawProperties())
+ .setClusterName(settingsMap.get("cluster.name"))
+ .setHost(settingsMap.get("network.host"))
+ .setPort(Integer.valueOf(settingsMap.get("transport.tcp.port")))
+ .addEsOption("-Epath.conf=" + esFileSystem.getConfDirectory().getAbsolutePath())
+ .setEsJvmOptions(new EsJvmOptions()
+ .addFromMandatoryProperty(props, ProcessProperties.SEARCH_JAVA_OPTS)
+ .addFromMandatoryProperty(props, ProcessProperties.SEARCH_JAVA_ADDITIONAL_OPTS))
+ .setEsYmlSettings(new EsYmlSettings(settingsMap))
+ .setEnvVariable("ES_JVM_OPTIONS", esFileSystem.getJvmOptions().getAbsolutePath())
+ .setEnvVariable("JAVA_HOME", System.getProperties().getProperty("java.home"))
+ .suppressEnvVariable(ENV_VAR_JAVA_TOOL_OPTIONS);
+ }
+
+ @Override
+ public JavaCommand createWebCommand(boolean leader) {
+ File homeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME);
+
+ WebJvmOptions jvmOptions = new WebJvmOptions(tempDir)
+ .addFromMandatoryProperty(props, ProcessProperties.WEB_JAVA_OPTS)
+ .addFromMandatoryProperty(props, ProcessProperties.WEB_JAVA_ADDITIONAL_OPTS);
+ addProxyJvmOptions(jvmOptions);
+
+ JavaCommand<WebJvmOptions> command = new JavaCommand<WebJvmOptions>(ProcessId.WEB_SERVER, homeDir)
+ .setArguments(props.rawProperties())
+ .setJvmOptions(jvmOptions)
+ // required for logback tomcat valve
+ .setEnvVariable(ProcessProperties.PATH_LOGS, props.nonNullValue(ProcessProperties.PATH_LOGS))
+ .setArgument("sonar.cluster.web.startupLeader", Boolean.toString(leader))
+ .setClassName("org.sonar.server.app.WebServer")
+ .addClasspath("./lib/common/*")
+ .addClasspath("./lib/server/*");
+ String driverPath = props.value(ProcessProperties.JDBC_DRIVER_PATH);
+ if (driverPath != null) {
+ command.addClasspath(driverPath);
+ }
+ command.suppressEnvVariable(ENV_VAR_JAVA_TOOL_OPTIONS);
+ return command;
+ }
+
+ @Override
+ public JavaCommand createCeCommand() {
+ File homeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME);
+
+ CeJvmOptions jvmOptions = new CeJvmOptions(tempDir)
+ .addFromMandatoryProperty(props, ProcessProperties.CE_JAVA_OPTS)
+ .addFromMandatoryProperty(props, ProcessProperties.CE_JAVA_ADDITIONAL_OPTS);
+ addProxyJvmOptions(jvmOptions);
+
+ JavaCommand<CeJvmOptions> command = new JavaCommand<CeJvmOptions>(ProcessId.COMPUTE_ENGINE, homeDir)
+ .setArguments(props.rawProperties())
+ .setJvmOptions(jvmOptions)
+ .setClassName("org.sonar.ce.app.CeServer")
+ .addClasspath("./lib/common/*")
+ .addClasspath("./lib/server/*")
+ .addClasspath("./lib/ce/*");
+ String driverPath = props.value(ProcessProperties.JDBC_DRIVER_PATH);
+ if (driverPath != null) {
+ command.addClasspath(driverPath);
+ }
+ command.suppressEnvVariable(ENV_VAR_JAVA_TOOL_OPTIONS);
+ return command;
+ }
+
+ private <T extends JvmOptions> void addProxyJvmOptions(JvmOptions<T> jvmOptions) {
+ for (String key : PROXY_PROPERTY_KEYS) {
+ getPropsValue(key).ifPresent(val -> jvmOptions.add("-D" + key + "=" + val));
+ }
+
+ // defaults of HTTPS are the same than HTTP defaults
+ setSystemPropertyToDefaultIfNotSet(jvmOptions, HTTPS_PROXY_HOST, HTTP_PROXY_HOST);
+ setSystemPropertyToDefaultIfNotSet(jvmOptions, HTTPS_PROXY_PORT, HTTP_PROXY_PORT);
+ }
+
+ private void setSystemPropertyToDefaultIfNotSet(JvmOptions jvmOptions,
+ String httpsProperty, String httpProperty) {
+ Optional<String> httpValue = getPropsValue(httpProperty);
+ Optional<String> httpsValue = getPropsValue(httpsProperty);
+ if (!httpsValue.isPresent() && httpValue.isPresent()) {
+ jvmOptions.add("-D" + httpsProperty + "=" + httpValue.get());
+ }
+ }
+
+ private Optional<String> getPropsValue(String key) {
+ return Optional.ofNullable(props.value(key));
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/EsCommand.java b/server/sonar-main/src/main/java/org/sonar/application/command/EsCommand.java
new file mode 100644
index 00000000000..d44e272132f
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/command/EsCommand.java
@@ -0,0 +1,118 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import org.sonar.application.es.EsFileSystem;
+import org.sonar.application.es.EsYmlSettings;
+import org.sonar.process.ProcessId;
+import org.sonar.process.System2;
+
+public class EsCommand extends AbstractCommand<EsCommand> {
+ private EsFileSystem fileSystem;
+ private String clusterName;
+ private String host;
+ private int port;
+ private Properties log4j2Properties;
+ private List<String> esOptions = new ArrayList<>();
+ private EsJvmOptions esJvmOptions;
+ private EsYmlSettings esYmlSettings;
+
+ public EsCommand(ProcessId id, File workDir) {
+ super(id, workDir, System2.INSTANCE);
+ }
+
+ public EsFileSystem getFileSystem() {
+ return fileSystem;
+ }
+
+ public EsCommand setFileSystem(EsFileSystem fileSystem) {
+ this.fileSystem = fileSystem;
+ return this;
+ }
+
+ public String getClusterName() {
+ return clusterName;
+ }
+
+ public EsCommand setClusterName(String clusterName) {
+ this.clusterName = clusterName;
+ return this;
+ }
+
+ public String getHost() {
+ return host;
+ }
+
+ public EsCommand setHost(String host) {
+ this.host = host;
+ return this;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public EsCommand setPort(int port) {
+ this.port = port;
+ return this;
+ }
+
+ public Properties getLog4j2Properties() {
+ return log4j2Properties;
+ }
+
+ public EsCommand setLog4j2Properties(Properties log4j2Properties) {
+ this.log4j2Properties = log4j2Properties;
+ return this;
+ }
+
+ public List<String> getEsOptions() {
+ return esOptions;
+ }
+
+ public EsCommand addEsOption(String s) {
+ if (!s.isEmpty()) {
+ esOptions.add(s);
+ }
+ return this;
+ }
+
+ public EsCommand setEsJvmOptions(EsJvmOptions esJvmOptions) {
+ this.esJvmOptions = esJvmOptions;
+ return this;
+ }
+
+ public EsJvmOptions getEsJvmOptions() {
+ return esJvmOptions;
+ }
+
+ public EsCommand setEsYmlSettings(EsYmlSettings esYmlSettings) {
+ this.esYmlSettings = esYmlSettings;
+ return this;
+ }
+
+ public EsYmlSettings getEsYmlSettings() {
+ return esYmlSettings;
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/EsJvmOptions.java b/server/sonar-main/src/main/java/org/sonar/application/command/EsJvmOptions.java
new file mode 100644
index 00000000000..bf2dfd680b3
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/command/EsJvmOptions.java
@@ -0,0 +1,71 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+public class EsJvmOptions extends JvmOptions<EsJvmOptions> {
+ private static final String ELASTICSEARCH_JVM_OPTIONS_HEADER = "# This file has been automatically generated by SonarQube during startup.\n" +
+ "# Please use sonar.search.javaOpts and/or sonar.search.javaAdditionalOpts in sonar.properties to specify jvm options for Elasticsearch\n" +
+ "\n" +
+ "# DO NOT EDIT THIS FILE\n" +
+ "\n";
+
+ public EsJvmOptions() {
+ super(mandatoryOptions());
+ }
+
+ private static Map<String, String> mandatoryOptions() {
+ Map<String, String> res = new LinkedHashMap<>(16);
+ res.put("-XX:+UseConcMarkSweepGC", "");
+ res.put("-XX:CMSInitiatingOccupancyFraction=", "75");
+ res.put("-XX:+UseCMSInitiatingOccupancyOnly", "");
+ res.put("-XX:+AlwaysPreTouch", "");
+ res.put("-server", "");
+ res.put("-Xss", "1m");
+ res.put("-Djava.awt.headless=", "true");
+ res.put("-Dfile.encoding=", "UTF-8");
+ res.put("-Djna.nosys=", "true");
+ res.put("-Djdk.io.permissionsUseCanonicalPath=", "true");
+ res.put("-Dio.netty.noUnsafe=", "true");
+ res.put("-Dio.netty.noKeySetOptimization=", "true");
+ res.put("-Dio.netty.recycler.maxCapacityPerThread=", "0");
+ res.put("-Dlog4j.shutdownHookEnabled=", "false");
+ res.put("-Dlog4j2.disable.jmx=", "true");
+ res.put("-Dlog4j.skipJansi=", "true");
+ return res;
+ }
+
+ public void writeToJvmOptionFile(File file) {
+ String jvmOptions = getAll().stream().collect(Collectors.joining("\n"));
+ String jvmOptionsContent = ELASTICSEARCH_JVM_OPTIONS_HEADER + jvmOptions;
+ try {
+ Files.write(file.toPath(), jvmOptionsContent.getBytes(Charset.forName("UTF-8")));
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot write Elasticsearch jvm options file", e);
+ }
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/JavaCommand.java b/server/sonar-main/src/main/java/org/sonar/application/command/JavaCommand.java
new file mode 100644
index 00000000000..5cee5a95e33
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/command/JavaCommand.java
@@ -0,0 +1,78 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.process.ProcessId;
+import org.sonar.process.System2;
+
+public class JavaCommand<T extends JvmOptions> extends AbstractCommand<JavaCommand<T>> {
+ // entry point
+ private String className;
+ private JvmOptions<T> jvmOptions;
+ // relative path to JAR files
+ private final List<String> classpath = new ArrayList<>();
+
+ public JavaCommand(ProcessId id, File workDir) {
+ super(id, workDir, System2.INSTANCE);
+ }
+
+ public JvmOptions<T> getJvmOptions() {
+ return jvmOptions;
+ }
+
+ public JavaCommand<T> setJvmOptions(JvmOptions<T> jvmOptions) {
+ this.jvmOptions = jvmOptions;
+
+ return this;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public JavaCommand<T> setClassName(String className) {
+ this.className = className;
+ return this;
+ }
+
+ public List<String> getClasspath() {
+ return classpath;
+ }
+
+ public JavaCommand<T> addClasspath(String s) {
+ classpath.add(s);
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "JavaCommand{" + "workDir=" + getWorkDir() +
+ ", jvmOptions=" + jvmOptions +
+ ", className='" + className + '\'' +
+ ", classpath=" + classpath +
+ ", arguments=" + getArguments() +
+ ", envVariables=" + getEnvVariables() +
+ ", suppressedEnvVariables=" + getSuppressedEnvVariables() +
+ '}';
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/JvmOptions.java b/server/sonar-main/src/main/java/org/sonar/application/command/JvmOptions.java
new file mode 100644
index 00000000000..3ef1b3bfd20
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/command/JvmOptions.java
@@ -0,0 +1,182 @@
+/*
+ * 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.command;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+import org.sonar.process.MessageException;
+import org.sonar.process.Props;
+
+import static java.lang.String.format;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.joining;
+
+public class JvmOptions<T extends JvmOptions> {
+ private static final String JVM_OPTION_NOT_NULL_ERROR_MESSAGE = "a JVM option can't be null";
+
+ private final HashMap<String, String> mandatoryOptions = new HashMap<>();
+ private final LinkedHashSet<String> options = new LinkedHashSet<>();
+
+ public JvmOptions() {
+ this(Collections.emptyMap());
+ }
+
+ public JvmOptions(Map<String, String> mandatoryJvmOptions) {
+ requireNonNull(mandatoryJvmOptions, JVM_OPTION_NOT_NULL_ERROR_MESSAGE)
+ .entrySet()
+ .stream()
+ .filter(e -> {
+ requireNonNull(e.getKey(), "JVM option prefix can't be null");
+ if (e.getKey().trim().isEmpty()) {
+ throw new IllegalArgumentException("JVM option prefix can't be empty");
+ }
+ requireNonNull(e.getValue(), "JVM option value can't be null");
+ return true;
+ }).forEach(e -> {
+ String key = e.getKey().trim();
+ String value = e.getValue().trim();
+ mandatoryOptions.put(key, value);
+ add(key + value);
+ });
+ }
+
+ public T addFromMandatoryProperty(Props props, String propertyName) {
+ String value = props.nonNullValue(propertyName);
+ if (!value.isEmpty()) {
+ List<String> jvmOptions = Arrays.stream(value.split(" (?=-)")).map(String::trim).collect(Collectors.toList());
+ checkOptionFormat(propertyName, jvmOptions);
+ checkMandatoryOptionOverwrite(propertyName, jvmOptions);
+ options.addAll(jvmOptions);
+ }
+
+ return castThis();
+ }
+
+ private static void checkOptionFormat(String propertyName, List<String> jvmOptionsFromProperty) {
+ List<String> invalidOptions = jvmOptionsFromProperty.stream()
+ .filter(JvmOptions::isInvalidOption)
+ .collect(Collectors.toList());
+ if (!invalidOptions.isEmpty()) {
+ throw new MessageException(format(
+ "a JVM option can't be empty and must start with '-'. The following JVM options defined by property '%s' are invalid: %s",
+ propertyName,
+ invalidOptions.stream()
+ .collect(joining(", "))));
+ }
+ }
+
+ private void checkMandatoryOptionOverwrite(String propertyName, List<String> jvmOptionsFromProperty) {
+ List<Match> matches = jvmOptionsFromProperty.stream()
+ .map(jvmOption -> new Match(jvmOption, mandatoryOptionFor(jvmOption)))
+ .filter(match -> match.getMandatoryOption() != null)
+ .collect(Collectors.toList());
+ if (!matches.isEmpty()) {
+ throw new MessageException(format(
+ "a JVM option can't overwrite mandatory JVM options. The following JVM options defined by property '%s' are invalid: %s",
+ propertyName,
+ matches.stream()
+ .map(m -> m.getOption() + " overwrites " + m.mandatoryOption.getKey() + m.mandatoryOption.getValue())
+ .collect(joining(", "))));
+ }
+ }
+
+ /**
+ * Add an option.
+ * Argument is trimmed before being added.
+ *
+ * @throws IllegalArgumentException if argument is empty or does not start with {@code -}.
+ */
+ public T add(String str) {
+ requireNonNull(str, JVM_OPTION_NOT_NULL_ERROR_MESSAGE);
+ String value = str.trim();
+ if (isInvalidOption(value)) {
+ throw new IllegalArgumentException("a JVM option can't be empty and must start with '-'");
+ }
+ checkMandatoryOptionOverwrite(value);
+ options.add(value);
+
+ return castThis();
+ }
+
+ private void checkMandatoryOptionOverwrite(String value) {
+ Map.Entry<String, String> overriddenMandatoryOption = mandatoryOptionFor(value);
+ if (overriddenMandatoryOption != null) {
+ throw new MessageException(String.format(
+ "a JVM option can't overwrite mandatory JVM options. %s overwrites %s",
+ value,
+ overriddenMandatoryOption.getKey() + overriddenMandatoryOption.getValue()));
+ }
+ }
+
+ @CheckForNull
+ private Map.Entry<String, String> mandatoryOptionFor(String jvmOption) {
+ return mandatoryOptions.entrySet().stream()
+ .filter(s -> jvmOption.startsWith(s.getKey()) && !jvmOption.equals(s.getKey() + s.getValue()))
+ .findFirst()
+ .orElse(null);
+ }
+
+ private static boolean isInvalidOption(String value) {
+ return value.isEmpty() || !value.startsWith("-");
+ }
+
+ @SuppressWarnings("unchecked")
+ private T castThis() {
+ return (T) this;
+ }
+
+ public List<String> getAll() {
+ return new ArrayList<>(options);
+ }
+
+ @Override
+ public String toString() {
+ return options.toString();
+ }
+
+ private static final class Match {
+ private final String option;
+
+ private final Map.Entry<String, String> mandatoryOption;
+
+ private Match(String option, @Nullable Map.Entry<String, String> mandatoryOption) {
+ this.option = option;
+ this.mandatoryOption = mandatoryOption;
+ }
+
+ String getOption() {
+ return option;
+ }
+
+ @CheckForNull
+ Map.Entry<String, String> getMandatoryOption() {
+ return mandatoryOption;
+ }
+
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/WebJvmOptions.java b/server/sonar-main/src/main/java/org/sonar/application/command/WebJvmOptions.java
new file mode 100644
index 00000000000..9a066a736f8
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/command/WebJvmOptions.java
@@ -0,0 +1,38 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class WebJvmOptions extends JvmOptions<WebJvmOptions> {
+ public WebJvmOptions(File tmpDir) {
+ super(mandatoryOptions(tmpDir));
+ }
+
+ private static Map<String, String> mandatoryOptions(File tmpDir) {
+ Map<String, String> res = new LinkedHashMap<>(3);
+ res.put("-Djava.awt.headless=", "true");
+ res.put("-Dfile.encoding=", "UTF-8");
+ res.put("-Djava.io.tmpdir=", tmpDir.getAbsolutePath());
+ return res;
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/command/package-info.java b/server/sonar-main/src/main/java/org/sonar/application/command/package-info.java
new file mode 100644
index 00000000000..a083f0ab81a
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/command/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.command;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsFileSystem.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsFileSystem.java
new file mode 100644
index 00000000000..73fc79edbe4
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsFileSystem.java
@@ -0,0 +1,105 @@
+/*
+ * 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.es;
+
+import java.io.File;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+
+/**
+ * Holds {@link File} to the various directories of ElasticSearch distribution embedded in SonarQube and provides
+ * {@link File} objects to the various files of it SonarQube cares about.
+ *
+ * <p>
+ * This class does not ensure files nor directories actually exist.
+ * </p>
+ */
+public class EsFileSystem {
+ private final File homeDirectory;
+ private final File dataDirectory;
+ private final File confDirectory;
+ private final File logDirectory;
+
+ public EsFileSystem(Props props) {
+ File sqHomeDir = props.nonNullValueAsFile(ProcessProperties.PATH_HOME);
+
+ this.homeDirectory = new File(sqHomeDir, "elasticsearch");
+ this.dataDirectory = buildDataDir(props, sqHomeDir);
+ this.confDirectory = buildConfDir(props);
+ this.logDirectory = buildLogPath(props);
+ }
+
+ private static File buildDataDir(Props props, File sqHomeDir) {
+ String dataPath = props.value(ProcessProperties.PATH_DATA);
+ if (StringUtils.isNotEmpty(dataPath)) {
+ return new File(dataPath, "es");
+ }
+ return new File(sqHomeDir, "data/es");
+ }
+
+ private static File buildLogPath(Props props) {
+ return props.nonNullValueAsFile(ProcessProperties.PATH_LOGS);
+ }
+
+ private static File buildConfDir(Props props) {
+ File tempPath = props.nonNullValueAsFile(ProcessProperties.PATH_TEMP);
+ return new File(new File(tempPath, "conf"), "es");
+ }
+
+ public File getHomeDirectory() {
+ return homeDirectory;
+ }
+
+ public File getDataDirectory() {
+ return dataDirectory;
+ }
+
+ public File getConfDirectory() {
+ return confDirectory;
+ }
+
+ public File getLogDirectory() {
+ return logDirectory;
+ }
+
+ public File getExecutable() {
+ return new File(homeDirectory, "bin/" + getExecutableName());
+ }
+
+ private static String getExecutableName() {
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ return "elasticsearch.bat";
+ }
+ return "elasticsearch";
+ }
+
+ public File getLog4j2Properties() {
+ return new File(confDirectory, "log4j2.properties");
+ }
+
+ public File getElasticsearchYml() {
+ return new File(confDirectory, "elasticsearch.yml");
+ }
+
+ public File getJvmOptions() {
+ return new File(confDirectory, "jvm.options");
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsLogging.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsLogging.java
new file mode 100644
index 00000000000..90fb1d74c2c
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsLogging.java
@@ -0,0 +1,50 @@
+/*
+ * 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.es;
+
+import ch.qos.logback.classic.Level;
+import java.io.File;
+import java.util.Properties;
+import org.sonar.process.ProcessId;
+import org.sonar.process.Props;
+import org.sonar.process.logging.Log4JPropertiesBuilder;
+import org.sonar.process.logging.LogLevelConfig;
+import org.sonar.process.logging.RootLoggerConfig;
+
+import static org.sonar.process.logging.RootLoggerConfig.newRootLoggerConfigBuilder;
+
+public class EsLogging {
+
+ public Properties createProperties(Props props, File logDir) {
+ Log4JPropertiesBuilder log4JPropertiesBuilder = new Log4JPropertiesBuilder(props);
+ RootLoggerConfig config = newRootLoggerConfigBuilder().setProcessId(ProcessId.ELASTICSEARCH).build();
+ String logPattern = log4JPropertiesBuilder.buildLogPattern(config);
+
+ log4JPropertiesBuilder.internalLogLevel(Level.ERROR);
+ log4JPropertiesBuilder.configureGlobalFileLog(config, logDir, logPattern);
+ log4JPropertiesBuilder.apply(
+ LogLevelConfig.newBuilder(log4JPropertiesBuilder.getRootLoggerName())
+ .rootLevelFor(ProcessId.ELASTICSEARCH)
+ .build());
+
+ return log4JPropertiesBuilder.get();
+ }
+
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java
new file mode 100644
index 00000000000..eb69acdbd61
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsSettings.java
@@ -0,0 +1,148 @@
+/*
+ * 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.es;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+import org.sonar.process.System2;
+
+import static java.lang.String.valueOf;
+import static org.sonar.cluster.ClusterProperties.CLUSTER_ENABLED;
+import static org.sonar.cluster.ClusterProperties.CLUSTER_NAME;
+import static org.sonar.cluster.ClusterProperties.CLUSTER_NODE_NAME;
+import static org.sonar.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS;
+
+public class EsSettings {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(EsSettings.class);
+ private static final String STANDALONE_NODE_NAME = "sonarqube";
+
+ private final Props props;
+ private final EsFileSystem fileSystem;
+
+ private final boolean clusterEnabled;
+ private final String clusterName;
+ private final String nodeName;
+
+ public EsSettings(Props props, EsFileSystem fileSystem, System2 system2) {
+ this.props = props;
+ this.fileSystem = fileSystem;
+
+ this.clusterName = props.nonNullValue(CLUSTER_NAME);
+ this.clusterEnabled = props.valueAsBoolean(CLUSTER_ENABLED);
+ if (this.clusterEnabled) {
+ this.nodeName = props.value(CLUSTER_NODE_NAME, "sonarqube-" + UUID.randomUUID().toString());
+ } else {
+ this.nodeName = STANDALONE_NODE_NAME;
+ }
+ String esJvmOptions = system2.getenv("ES_JVM_OPTIONS");
+ if (esJvmOptions != null && !esJvmOptions.trim().isEmpty()) {
+ LOGGER.warn("ES_JVM_OPTIONS is defined but will be ignored. " +
+ "Use sonar.search.javaOpts and/or sonar.search.javaAdditionalOpts in sonar.properties to specify jvm options for Elasticsearch");
+ }
+ }
+
+ public Map<String, String> build() {
+ Map<String, String> builder = new HashMap<>();
+ configureFileSystem(builder);
+ configureNetwork(builder);
+ configureCluster(builder);
+ configureAction(builder);
+ return builder;
+ }
+
+ private void configureFileSystem(Map<String, String> builder) {
+ builder.put("path.data", fileSystem.getDataDirectory().getAbsolutePath());
+ builder.put("path.conf", fileSystem.getConfDirectory().getAbsolutePath());
+ builder.put("path.logs", fileSystem.getLogDirectory().getAbsolutePath());
+ }
+
+ private void configureNetwork(Map<String, String> builder) {
+ InetAddress host = readHost();
+ int port = Integer.parseInt(props.nonNullValue(ProcessProperties.SEARCH_PORT));
+ LOGGER.info("Elasticsearch listening on {}:{}", host, port);
+
+ builder.put("transport.tcp.port", valueOf(port));
+ builder.put("transport.host", valueOf(host.getHostAddress()));
+ builder.put("network.host", valueOf(host.getHostAddress()));
+
+ // Elasticsearch sets the default value of TCP reuse address to true only on non-MSWindows machines, but why ?
+ builder.put("network.tcp.reuse_address", valueOf(true));
+
+ int httpPort = props.valueAsInt(ProcessProperties.SEARCH_HTTP_PORT, -1);
+ if (httpPort < 0) {
+ // standard configuration
+ builder.put("http.enabled", valueOf(false));
+ } else {
+ LOGGER.warn("Elasticsearch HTTP connector is enabled on port {}. MUST NOT BE USED FOR PRODUCTION", httpPort);
+ // see https://github.com/lmenezes/elasticsearch-kopf/issues/195
+ builder.put("http.cors.enabled", valueOf(true));
+ builder.put("http.cors.allow-origin", "*");
+ builder.put("http.enabled", valueOf(true));
+ builder.put("http.host", host.getHostAddress());
+ builder.put("http.port", valueOf(httpPort));
+ }
+ }
+
+ private InetAddress readHost() {
+ String hostProperty = props.nonNullValue(ProcessProperties.SEARCH_HOST);
+ try {
+ return InetAddress.getByName(hostProperty);
+ } catch (UnknownHostException e) {
+ throw new IllegalStateException("Can not resolve host [" + hostProperty + "]. Please check network settings and property " + ProcessProperties.SEARCH_HOST, e);
+ }
+ }
+
+ private void configureCluster(Map<String, String> builder) {
+ // Default value in a standalone mode, not overridable
+
+ int minimumMasterNodes = 1;
+ String initialStateTimeOut = "30s";
+
+ if (clusterEnabled) {
+ minimumMasterNodes = props.valueAsInt(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, 2);
+ initialStateTimeOut = props.value(ProcessProperties.SEARCH_INITIAL_STATE_TIMEOUT, "120s");
+
+ String hosts = props.value(CLUSTER_SEARCH_HOSTS, "");
+ LOGGER.info("Elasticsearch cluster enabled. Connect to hosts [{}]", hosts);
+ builder.put("discovery.zen.ping.unicast.hosts", hosts);
+ }
+
+ builder.put("discovery.zen.minimum_master_nodes", valueOf(minimumMasterNodes));
+ builder.put("discovery.initial_state_timeout", initialStateTimeOut);
+ builder.put("cluster.name", clusterName);
+ builder.put("cluster.routing.allocation.awareness.attributes", "rack_id");
+ builder.put("node.attr.rack_id", nodeName);
+ builder.put("node.name", nodeName);
+ builder.put("node.data", valueOf(true));
+ builder.put("node.master", valueOf(true));
+ }
+
+ private static void configureAction(Map<String, String> builder) {
+ builder.put("action.auto_create_index", String.valueOf(false));
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/EsYmlSettings.java b/server/sonar-main/src/main/java/org/sonar/application/es/EsYmlSettings.java
new file mode 100644
index 00000000000..3ba92d55cbc
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/es/EsYmlSettings.java
@@ -0,0 +1,56 @@
+/*
+ * 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.es;
+
+import java.io.File;
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.util.Map;
+import org.yaml.snakeyaml.DumperOptions;
+import org.yaml.snakeyaml.Yaml;
+
+import static org.yaml.snakeyaml.DumperOptions.FlowStyle.BLOCK;
+
+public class EsYmlSettings {
+ private static final String ELASTICSEARCH_YML_OPTIONS_HEADER = "# This file has been automatically generated by SonarQube during startup.\n" +
+ "\n" +
+ "# DO NOT EDIT THIS FILE\n" +
+ "\n";
+
+ private final Map<String, String> elasticsearchSettings;
+
+ public EsYmlSettings(Map<String, String> elasticsearchSettings) {
+ this.elasticsearchSettings = elasticsearchSettings;
+ }
+
+ public void writeToYmlSettingsFile(File file) {
+ DumperOptions dumperOptions = new DumperOptions();
+ dumperOptions.setPrettyFlow(true);
+ dumperOptions.setDefaultFlowStyle(BLOCK);
+ Yaml yaml = new Yaml(dumperOptions);
+ String output = ELASTICSEARCH_YML_OPTIONS_HEADER + yaml.dump(elasticsearchSettings);
+ try {
+ Files.write(file.toPath(), output.getBytes(Charset.forName("UTF-8")));
+ } catch (IOException e) {
+ throw new IllegalStateException("Cannot write Elasticsearch yml settings file", e);
+ }
+ }
+}
diff --git a/server/sonar-main/src/main/java/org/sonar/application/es/package-info.java b/server/sonar-main/src/main/java/org/sonar/application/es/package-info.java
new file mode 100644
index 00000000000..8f296059727
--- /dev/null
+++ b/server/sonar-main/src/main/java/org/sonar/application/es/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * 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.es;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-main/src/main/java/org/sonar/application/process/EsProcessMonitor.java b/server/sonar-main/src/main/java/org/sonar/application/process/EsProcessMonitor.java
index e4730c7303d..fd611188f95 100644
--- a/server/sonar-main/src/main/java/org/sonar/application/process/EsProcessMonitor.java
+++ b/server/sonar-main/src/main/java/org/sonar/application/process/EsProcessMonitor.java
@@ -38,7 +38,7 @@ import org.elasticsearch.discovery.MasterNotDiscoveredException;
import org.elasticsearch.transport.Netty4Plugin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.sonar.process.command.EsCommand;
+import org.sonar.application.command.EsCommand;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
diff --git a/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncher.java b/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncher.java
index c39f91bc8fa..c0ca8ec7825 100644
--- a/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncher.java
+++ b/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncher.java
@@ -20,8 +20,8 @@
package org.sonar.application.process;
import java.io.Closeable;
-import org.sonar.process.command.EsCommand;
-import org.sonar.process.command.JavaCommand;
+import org.sonar.application.command.EsCommand;
+import org.sonar.application.command.JavaCommand;
public interface ProcessLauncher extends Closeable {
diff --git a/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java b/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java
index b0b968f05eb..1a8daf01ae3 100644
--- a/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java
+++ b/server/sonar-main/src/main/java/org/sonar/application/process/ProcessLauncherImpl.java
@@ -31,12 +31,12 @@ import java.util.Properties;
import java.util.function.Supplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.sonar.application.command.AbstractCommand;
+import org.sonar.application.command.EsCommand;
+import org.sonar.application.command.JavaCommand;
+import org.sonar.application.command.JvmOptions;
+import org.sonar.application.es.EsFileSystem;
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.es.EsFileSystem;
-import org.sonar.process.jmvoptions.JvmOptions;
import org.sonar.process.sharedmemoryfile.AllProcessesCommands;
import org.sonar.process.sharedmemoryfile.ProcessCommands;
diff --git a/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java
index f419f4d0e17..a2ada4bb3f8 100644
--- a/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java
+++ b/server/sonar-main/src/test/java/org/sonar/application/SchedulerImplTest.java
@@ -43,10 +43,10 @@ import org.sonar.application.process.ProcessLauncher;
import org.sonar.application.process.ProcessMonitor;
import org.sonar.cluster.localclient.HazelcastClient;
import org.sonar.process.ProcessId;
-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 org.sonar.application.command.AbstractCommand;
+import org.sonar.application.command.CommandFactory;
+import org.sonar.application.command.EsCommand;
+import org.sonar.application.command.JavaCommand;
import static java.util.Collections.synchronizedList;
import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/AbstractCommandTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/AbstractCommandTest.java
new file mode 100644
index 00000000000..c5ff76bc4dc
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/command/AbstractCommandTest.java
@@ -0,0 +1,148 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Random;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+import org.sonar.process.ProcessId;
+import org.sonar.process.System2;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+public class AbstractCommandTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void constructor_throws_NPE_of_ProcessId_is_null() throws IOException {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("ProcessId can't be null");
+
+ new AbstractCommand<AbstractCommand>(null, temp.newFolder(), System2.INSTANCE) {
+
+ };
+ }
+
+ @Test
+ public void constructor_throws_NPE_of_workDir_is_null() throws IOException {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("workDir can't be null");
+
+ new AbstractCommand<AbstractCommand>(ProcessId.WEB_SERVER, null, System2.INSTANCE) {
+
+ };
+ }
+
+ @Test
+ public void test_command_with_complete_information() throws Exception {
+ File workDir = temp.newFolder();
+ AbstractCommand command = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir, System2.INSTANCE) {
+
+ };
+
+ command.setArgument("first_arg", "val1");
+ Properties args = new Properties();
+ args.setProperty("second_arg", "val2");
+ command.setArguments(args);
+
+ command.setEnvVariable("JAVA_COMMAND_TEST", "1000");
+
+ assertThat(command.toString()).isNotNull();
+ assertThat(command.getWorkDir()).isSameAs(workDir);
+
+ // copy current env variables
+ assertThat(command.getEnvVariables().get("JAVA_COMMAND_TEST")).isEqualTo("1000");
+ assertThat(command.getEnvVariables().size()).isEqualTo(System.getenv().size() + 1);
+ }
+
+ @Test
+ public void setEnvVariable_fails_with_NPE_if_key_is_null() throws IOException {
+ File workDir = temp.newFolder();
+ AbstractCommand underTest = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir, System2.INSTANCE) {
+
+ };
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("key can't be null");
+
+ underTest.setEnvVariable(null, randomAlphanumeric(30));
+ }
+
+ @Test
+ public void setEnvVariable_fails_with_NPE_if_value_is_null() throws IOException {
+ File workDir = temp.newFolder();
+ AbstractCommand underTest = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir, System2.INSTANCE) {
+
+ };
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("value can't be null");
+
+ underTest.setEnvVariable(randomAlphanumeric(30), null);
+ }
+
+ @Test
+ public void constructor_puts_System_getEnv_into_map_of_env_variables() throws IOException {
+ File workDir = temp.newFolder();
+ System2 system2 = Mockito.mock(System2.class);
+ Map<String, String> env = IntStream.range(0, 1 + new Random().nextInt(99)).mapToObj(String::valueOf).collect(Collectors.toMap(i -> "key" + i, j -> "value" + j));
+ when(system2.getenv()).thenReturn(env);
+ AbstractCommand underTest = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir, system2) {
+
+ };
+
+ assertThat(underTest.getEnvVariables()).isEqualTo(env);
+ }
+
+ @Test
+ public void suppressEnvVariable_remove_existing_env_variable_and_add_variable_to_set_of_suppressed_variables() throws IOException {
+ File workDir = temp.newFolder();
+ System2 system2 = Mockito.mock(System2.class);
+ Map<String, String> env = new HashMap<>();
+ String key1 = randomAlphanumeric(3);
+ env.put(key1, randomAlphanumeric(9));
+ when(system2.getenv()).thenReturn(env);
+ AbstractCommand underTest = new AbstractCommand(ProcessId.ELASTICSEARCH, workDir, system2) {
+
+ };
+
+ underTest.suppressEnvVariable(key1);
+
+ assertThat(underTest.getEnvVariables()).doesNotContainKey(key1);
+ assertThat(underTest.getSuppressedEnvVariables()).containsOnly(key1);
+ }
+
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/CeJvmOptionsTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/CeJvmOptionsTest.java
new file mode 100644
index 00000000000..4084336249c
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/command/CeJvmOptionsTest.java
@@ -0,0 +1,42 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CeJvmOptionsTest {
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void constructor_sets_mandatory_JVM_options() throws IOException {
+ File tmpDir = temporaryFolder.newFolder();
+ CeJvmOptions underTest = new CeJvmOptions(tmpDir);
+
+ assertThat(underTest.getAll()).containsExactly(
+ "-Djava.awt.headless=true", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=" + tmpDir.getAbsolutePath());
+ }
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/CommandFactoryImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/CommandFactoryImplTest.java
new file mode 100644
index 00000000000..ceb13a08dac
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/command/CommandFactoryImplTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.command;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import java.io.File;
+import java.io.IOException;
+import java.util.Properties;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.mockito.Mockito;
+import org.sonar.process.ProcessId;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+import org.sonar.process.System2;
+import org.sonar.application.logging.ListAppender;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.when;
+
+public class CommandFactoryImplTest {
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ private File homeDir;
+ private File tempDir;
+ private File logsDir;
+ private ListAppender listAppender;
+
+ @Before
+ public void setUp() throws Exception {
+ homeDir = temp.newFolder();
+ tempDir = temp.newFolder();
+ logsDir = temp.newFolder();
+ }
+
+ @After
+ public void tearDown() throws Exception {
+ if (listAppender != null) {
+ ListAppender.detachMemoryAppenderToLoggerOf(CommandFactoryImpl.class, listAppender);
+ }
+ }
+
+ @Test
+ public void constructor_logs_no_warning_if_env_variable_JAVA_TOOL_OPTIONS_is_not_set() {
+ System2 system2 = Mockito.mock(System2.class);
+ when(system2.getenv(anyString())).thenReturn(null);
+ attachMemoryAppenderToLoggerOf(CommandFactoryImpl.class);
+
+ new CommandFactoryImpl(new Props(new Properties()), tempDir, system2);
+
+ assertThat(listAppender.getLogs()).isEmpty();
+ }
+
+ @Test
+ public void constructor_logs_warning_if_env_variable_JAVA_TOOL_OPTIONS_is_set() {
+ System2 system2 = Mockito.mock(System2.class);
+ when(system2.getenv("JAVA_TOOL_OPTIONS")).thenReturn("sds");
+ attachMemoryAppenderToLoggerOf(CommandFactoryImpl.class);
+
+ new CommandFactoryImpl(new Props(new Properties()), tempDir, system2);
+
+ assertThat(listAppender.getLogs())
+ .extracting(ILoggingEvent::getMessage)
+ .containsOnly(
+ "JAVA_TOOL_OPTIONS is defined but will be ignored. " +
+ "Use properties sonar.*.javaOpts and/or sonar.*.javaAdditionalOpts in sonar.properties to change SQ JVM processes options");
+ }
+
+ @Test
+ public void createEsCommand_throws_ISE_if_es_binary_is_not_found() throws Exception {
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Cannot find elasticsearch binary");
+
+ newFactory(new Properties()).createEsCommand();
+ }
+
+ @Test
+ public void createEsCommand_returns_command_for_default_settings() throws Exception {
+ prepareEsFileSystem();
+
+ Properties props = new Properties();
+ props.setProperty("sonar.search.host", "localhost");
+
+ EsCommand esCommand = newFactory(props).createEsCommand();
+
+ assertThat(esCommand.getClusterName()).isEqualTo("sonarqube");
+ assertThat(esCommand.getHost()).isNotEmpty();
+ assertThat(esCommand.getPort()).isEqualTo(9001);
+ assertThat(esCommand.getEsJvmOptions().getAll())
+ // enforced values
+ .contains("-XX:+UseConcMarkSweepGC", "-server", "-Dfile.encoding=UTF-8")
+ // default settings
+ .contains("-Xms512m", "-Xmx512m", "-XX:+HeapDumpOnOutOfMemoryError");
+ File esConfDir = new File(tempDir, "conf/es");
+ assertThat(esCommand.getEsOptions()).containsOnly("-Epath.conf=" + esConfDir.getAbsolutePath());
+ assertThat(esCommand.getEnvVariables())
+ .contains(entry("ES_JVM_OPTIONS", new File(esConfDir, "jvm.options").getAbsolutePath()))
+ .containsKey("JAVA_HOME");
+ assertThat(esCommand.getEsYmlSettings()).isNotNull();
+
+ assertThat(esCommand.getLog4j2Properties())
+ .contains(entry("appender.file_es.fileName", new File(logsDir, "es.log").getAbsolutePath()));
+
+ assertThat(esCommand.getSuppressedEnvVariables()).containsOnly("JAVA_TOOL_OPTIONS");
+ }
+
+ @Test
+ public void createEsCommand_returns_command_for_overridden_settings() throws Exception {
+ prepareEsFileSystem();
+
+ Properties props = new Properties();
+ props.setProperty("sonar.search.host", "localhost");
+ props.setProperty("sonar.cluster.name", "foo");
+ props.setProperty("sonar.search.port", "1234");
+ props.setProperty("sonar.search.javaOpts", "-Xms10G -Xmx10G");
+
+ EsCommand command = newFactory(props).createEsCommand();
+
+ assertThat(command.getClusterName()).isEqualTo("foo");
+ assertThat(command.getPort()).isEqualTo(1234);
+ assertThat(command.getEsJvmOptions().getAll())
+ // enforced values
+ .contains("-XX:+UseConcMarkSweepGC", "-server", "-Dfile.encoding=UTF-8")
+ // user settings
+ .contains("-Xms10G", "-Xmx10G")
+ // default values disabled
+ .doesNotContain("-XX:+HeapDumpOnOutOfMemoryError");
+ }
+
+ @Test
+ public void createWebCommand_returns_command_for_default_settings() throws Exception {
+ JavaCommand command = newFactory(new Properties()).createWebCommand(true);
+
+ assertThat(command.getClassName()).isEqualTo("org.sonar.server.app.WebServer");
+ assertThat(command.getWorkDir().getAbsolutePath()).isEqualTo(homeDir.getAbsolutePath());
+ assertThat(command.getClasspath())
+ .containsExactlyInAnyOrder("./lib/common/*", "./lib/server/*");
+ assertThat(command.getJvmOptions().getAll())
+ // enforced values
+ .contains("-Djava.awt.headless=true", "-Dfile.encoding=UTF-8")
+ // default settings
+ .contains("-Djava.io.tmpdir=" + tempDir.getAbsolutePath(), "-Dfile.encoding=UTF-8")
+ .contains("-Xmx512m", "-Xms128m", "-XX:+HeapDumpOnOutOfMemoryError");
+ assertThat(command.getProcessId()).isEqualTo(ProcessId.WEB_SERVER);
+ assertThat(command.getEnvVariables())
+ .containsKey("JAVA_HOME");
+ assertThat(command.getArguments())
+ // default settings
+ .contains(entry("sonar.web.javaOpts", "-Xmx512m -Xms128m -XX:+HeapDumpOnOutOfMemoryError"))
+ .contains(entry("sonar.cluster.enabled", "false"));
+
+ assertThat(command.getSuppressedEnvVariables()).containsOnly("JAVA_TOOL_OPTIONS");
+ }
+
+ @Test
+ public void createWebCommand_configures_command_with_overridden_settings() throws Exception {
+ Properties props = new Properties();
+ props.setProperty("sonar.web.port", "1234");
+ props.setProperty("sonar.web.javaOpts", "-Xmx10G");
+ JavaCommand command = newFactory(props).createWebCommand(true);
+
+ assertThat(command.getJvmOptions().getAll())
+ // enforced values
+ .contains("-Djava.awt.headless=true", "-Dfile.encoding=UTF-8")
+ // default settings
+ .contains("-Djava.io.tmpdir=" + tempDir.getAbsolutePath(), "-Dfile.encoding=UTF-8")
+ // overridden values
+ .contains("-Xmx10G")
+ .doesNotContain("-Xms128m", "-XX:+HeapDumpOnOutOfMemoryError");
+ assertThat(command.getArguments())
+ // default settings
+ .contains(entry("sonar.web.javaOpts", "-Xmx10G"))
+ .contains(entry("sonar.cluster.enabled", "false"));
+
+ assertThat(command.getSuppressedEnvVariables()).containsOnly("JAVA_TOOL_OPTIONS");
+ }
+
+ @Test
+ public void createWebCommand_adds_configured_jdbc_driver_to_classpath() throws Exception {
+ Properties props = new Properties();
+ File driverFile = temp.newFile();
+ props.setProperty("sonar.jdbc.driverPath", driverFile.getAbsolutePath());
+
+ JavaCommand command = newFactory(props).createWebCommand(true);
+
+ assertThat(command.getClasspath())
+ .containsExactlyInAnyOrder("./lib/common/*", "./lib/server/*", driverFile.getAbsolutePath());
+ }
+
+ private void prepareEsFileSystem() throws IOException {
+ FileUtils.touch(new File(homeDir, "elasticsearch/bin/elasticsearch"));
+ FileUtils.touch(new File(homeDir, "elasticsearch/bin/elasticsearch.bat"));
+ }
+
+ private CommandFactory newFactory(Properties userProps) throws IOException {
+ Properties p = new Properties();
+ p.setProperty("sonar.path.home", homeDir.getAbsolutePath());
+ p.setProperty("sonar.path.temp", tempDir.getAbsolutePath());
+ p.setProperty("sonar.path.logs", logsDir.getAbsolutePath());
+ p.putAll(userProps);
+
+ Props props = new Props(p);
+ ProcessProperties.completeDefaults(props);
+ return new CommandFactoryImpl(props, tempDir, System2.INSTANCE);
+ }
+
+ private <T> void attachMemoryAppenderToLoggerOf(Class<T> loggerClass) {
+ this.listAppender = ListAppender.attachMemoryAppenderToLoggerOf(loggerClass);
+ }
+
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/EsJvmOptionsTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/EsJvmOptionsTest.java
new file mode 100644
index 00000000000..9f4aa5efee3
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/command/EsJvmOptionsTest.java
@@ -0,0 +1,108 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.test.ExceptionCauseMatcher;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EsJvmOptionsTest {
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void constructor_sets_mandatory_JVM_options() {
+ EsJvmOptions underTest = new EsJvmOptions();
+
+ assertThat(underTest.getAll()).containsExactly(
+ "-XX:+UseConcMarkSweepGC",
+ "-XX:CMSInitiatingOccupancyFraction=75",
+ "-XX:+UseCMSInitiatingOccupancyOnly",
+ "-XX:+AlwaysPreTouch",
+ "-server",
+ "-Xss1m",
+ "-Djava.awt.headless=true",
+ "-Dfile.encoding=UTF-8",
+ "-Djna.nosys=true",
+ "-Djdk.io.permissionsUseCanonicalPath=true",
+ "-Dio.netty.noUnsafe=true",
+ "-Dio.netty.noKeySetOptimization=true",
+ "-Dio.netty.recycler.maxCapacityPerThread=0",
+ "-Dlog4j.shutdownHookEnabled=false",
+ "-Dlog4j2.disable.jmx=true",
+ "-Dlog4j.skipJansi=true");
+ }
+
+ @Test
+ public void writeToJvmOptionFile_writes_all_JVM_options_to_file_with_warning_header() throws IOException {
+ File file = temporaryFolder.newFile();
+ EsJvmOptions underTest = new EsJvmOptions()
+ .add("-foo")
+ .add("-bar");
+
+ underTest.writeToJvmOptionFile(file);
+
+ assertThat(file).hasContent(
+ "# This file has been automatically generated by SonarQube during startup.\n" +
+ "# Please use sonar.search.javaOpts and/or sonar.search.javaAdditionalOpts in sonar.properties to specify jvm options for Elasticsearch\n" +
+ "\n" +
+ "# DO NOT EDIT THIS FILE\n" +
+ "\n" +
+ "-XX:+UseConcMarkSweepGC\n" +
+ "-XX:CMSInitiatingOccupancyFraction=75\n" +
+ "-XX:+UseCMSInitiatingOccupancyOnly\n" +
+ "-XX:+AlwaysPreTouch\n" +
+ "-server\n" +
+ "-Xss1m\n" +
+ "-Djava.awt.headless=true\n" +
+ "-Dfile.encoding=UTF-8\n" +
+ "-Djna.nosys=true\n" +
+ "-Djdk.io.permissionsUseCanonicalPath=true\n" +
+ "-Dio.netty.noUnsafe=true\n" +
+ "-Dio.netty.noKeySetOptimization=true\n" +
+ "-Dio.netty.recycler.maxCapacityPerThread=0\n" +
+ "-Dlog4j.shutdownHookEnabled=false\n" +
+ "-Dlog4j2.disable.jmx=true\n" +
+ "-Dlog4j.skipJansi=true\n" +
+ "-foo\n" +
+ "-bar");
+
+ }
+
+ @Test
+ public void writeToJvmOptionFile_throws_ISE_in_case_of_IOException() throws IOException {
+ File notAFile = temporaryFolder.newFolder();
+ EsJvmOptions underTest = new EsJvmOptions();
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Cannot write Elasticsearch jvm options file");
+ expectedException.expectCause(ExceptionCauseMatcher.hasType(IOException.class));
+
+ underTest.writeToJvmOptionFile(notAFile);
+ }
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/JavaCommandTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/JavaCommandTest.java
new file mode 100644
index 00000000000..bd39967ccc9
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/command/JavaCommandTest.java
@@ -0,0 +1,64 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.util.Properties;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.process.ProcessId;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class JavaCommandTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void test_command_with_complete_information() throws Exception {
+ File workDir = temp.newFolder();
+ JavaCommand<JvmOptions> command = new JavaCommand<>(ProcessId.ELASTICSEARCH, workDir);
+
+ command.setArgument("first_arg", "val1");
+ Properties args = new Properties();
+ args.setProperty("second_arg", "val2");
+ command.setArguments(args);
+
+ command.setClassName("org.sonar.ElasticSearch");
+ command.setEnvVariable("JAVA_COMMAND_TEST", "1000");
+ command.addClasspath("lib/*.jar");
+ command.addClasspath("conf/*.xml");
+ JvmOptions<JvmOptions> jvmOptions = new JvmOptions<JvmOptions>() {};
+ command.setJvmOptions(jvmOptions);
+
+ assertThat(command.toString()).isNotNull();
+ assertThat(command.getClasspath()).containsOnly("lib/*.jar", "conf/*.xml");
+ assertThat(command.getJvmOptions()).isSameAs(jvmOptions);
+ assertThat(command.getWorkDir()).isSameAs(workDir);
+ assertThat(command.getClassName()).isEqualTo("org.sonar.ElasticSearch");
+
+ // copy current env variables
+ assertThat(command.getEnvVariables().get("JAVA_COMMAND_TEST")).isEqualTo("1000");
+ assertThat(command.getEnvVariables().size()).isEqualTo(System.getenv().size() + 1);
+ }
+
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/JvmOptionsTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/JvmOptionsTest.java
new file mode 100644
index 00000000000..038506939a8
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/command/JvmOptionsTest.java
@@ -0,0 +1,427 @@
+/*
+ * 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.command;
+
+import com.google.common.collect.ImmutableMap;
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Random;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.process.MessageException;
+import org.sonar.process.Props;
+
+import static java.lang.String.valueOf;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+
+@RunWith(DataProviderRunner.class)
+public class JvmOptionsTest {
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ private final Random random = new Random();
+ private final String randomPropertyName = randomAlphanumeric(3);
+ private final String randomPrefix = "-" + randomAlphabetic(5).toLowerCase(Locale.ENGLISH);
+ private final String randomValue = randomAlphanumeric(4).toLowerCase(Locale.ENGLISH);
+ private final Properties properties = new Properties();
+ private final JvmOptions underTest = new JvmOptions();
+
+ @Test
+ public void constructor_without_arguments_creates_empty_JvmOptions() {
+ JvmOptions<JvmOptions> testJvmOptions = new JvmOptions<>();
+
+ assertThat(testJvmOptions.getAll()).isEmpty();
+ }
+
+ @Test
+ public void constructor_throws_NPE_if_argument_is_null() {
+ expectJvmOptionNotNullNPE();
+
+ new JvmOptions(null);
+ }
+
+ @Test
+ public void constructor_throws_NPE_if_any_option_prefix_is_null() {
+ Map<String, String> mandatoryJvmOptions = shuffleThenToMap(
+ Stream.of(
+ IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))),
+ Stream.of(new Option(null, "value")))
+ .flatMap(s -> s));
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("JVM option prefix can't be null");
+
+ new JvmOptions(mandatoryJvmOptions);
+ }
+
+ @Test
+ @UseDataProvider("variousEmptyStrings")
+ public void constructor_throws_IAE_if_any_option_prefix_is_empty(String emptyString) {
+ Map<String, String> mandatoryJvmOptions = shuffleThenToMap(
+ Stream.of(
+ IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))),
+ Stream.of(new Option(emptyString, "value")))
+ .flatMap(s -> s));
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("JVM option prefix can't be empty");
+
+ new JvmOptions(mandatoryJvmOptions);
+ }
+
+ @Test
+ public void constructor_throws_IAE_if_any_option_prefix_does_not_start_with_dash() {
+ String invalidPrefix = randomAlphanumeric(3);
+ Map<String, String> mandatoryJvmOptions = shuffleThenToMap(
+ Stream.of(
+ IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))),
+ Stream.of(new Option(invalidPrefix, "value")))
+ .flatMap(s -> s));
+
+ expectJvmOptionNotEmptyAndStartByDashIAE();
+
+ new JvmOptions(mandatoryJvmOptions);
+ }
+
+ @Test
+ public void constructor_throws_NPE_if_any_option_value_is_null() {
+ Map<String, String> mandatoryJvmOptions = shuffleThenToMap(
+ Stream.of(
+ IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))),
+ Stream.of(new Option("-prefix", null)))
+ .flatMap(s -> s));
+
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("JVM option value can't be null");
+
+ new JvmOptions(mandatoryJvmOptions);
+ }
+
+ @Test
+ @UseDataProvider("variousEmptyStrings")
+ public void constructor_accepts_any_empty_option_value(String emptyString) {
+ Map<String, String> mandatoryJvmOptions = shuffleThenToMap(
+ Stream.of(
+ IntStream.range(0, random.nextInt(10)).mapToObj(i -> new Option("-B", valueOf(i))),
+ Stream.of(new Option("-prefix", emptyString)))
+ .flatMap(s -> s));
+
+ new JvmOptions(mandatoryJvmOptions);
+ }
+
+ @Test
+ public void add_throws_NPE_if_argument_is_null() {
+ expectJvmOptionNotNullNPE();
+
+ underTest.add(null);
+ }
+
+ @Test
+ @UseDataProvider("variousEmptyStrings")
+ public void add_throws_IAE_if_argument_is_empty(String emptyString) {
+ expectJvmOptionNotEmptyAndStartByDashIAE();
+
+ underTest.add(emptyString);
+ }
+
+ @Test
+ public void add_throws_IAE_if_argument_does_not_start_with_dash() {
+ expectJvmOptionNotEmptyAndStartByDashIAE();
+
+ underTest.add(randomAlphanumeric(3));
+ }
+
+ @Test
+ @UseDataProvider("variousEmptyStrings")
+ public void add_adds_with_trimming(String emptyString) {
+ underTest.add(emptyString + "-foo" + emptyString);
+
+ assertThat(underTest.getAll()).containsOnly("-foo");
+ }
+
+ @Test
+ public void add_throws_MessageException_if_option_starts_with_prefix_of_mandatory_option_but_has_different_value() {
+ String[] optionOverrides = {
+ randomPrefix,
+ randomPrefix + randomAlphanumeric(1),
+ randomPrefix + randomAlphanumeric(2),
+ randomPrefix + randomAlphanumeric(3),
+ randomPrefix + randomAlphanumeric(4),
+ randomPrefix + randomValue.substring(1),
+ randomPrefix + randomValue.substring(2),
+ randomPrefix + randomValue.substring(3)
+ };
+
+ JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue));
+
+ for (String optionOverride : optionOverrides) {
+ try {
+ underTest.add(optionOverride);
+ fail("an MessageException should have been thrown");
+ } catch (MessageException e) {
+ assertThat(e.getMessage()).isEqualTo("a JVM option can't overwrite mandatory JVM options. " + optionOverride + " overwrites " + randomPrefix + randomValue);
+ }
+ }
+ }
+
+ @Test
+ public void add_checks_against_mandatory_options_is_case_sensitive() {
+ String[] optionOverrides = {
+ randomPrefix,
+ randomPrefix + randomAlphanumeric(1),
+ randomPrefix + randomAlphanumeric(2),
+ randomPrefix + randomAlphanumeric(3),
+ randomPrefix + randomAlphanumeric(4),
+ randomPrefix + randomValue.substring(1),
+ randomPrefix + randomValue.substring(2),
+ randomPrefix + randomValue.substring(3)
+ };
+
+ JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue));
+
+ for (String optionOverride : optionOverrides) {
+ underTest.add(optionOverride.toUpperCase(Locale.ENGLISH));
+ }
+ }
+
+ @Test
+ public void add_accepts_property_equal_to_mandatory_option_and_does_not_add_it_twice() {
+ JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue));
+
+ underTest.add(randomPrefix + randomValue);
+
+ assertThat(underTest.getAll()).containsOnly(randomPrefix + randomValue);
+ }
+
+ @Test
+ public void addFromMandatoryProperty_fails_with_IAE_if_property_does_not_exist() {
+ expectMissingPropertyIAE(this.randomPropertyName);
+
+ underTest.addFromMandatoryProperty(new Props(properties), this.randomPropertyName);
+ }
+
+ @Test
+ public void addFromMandatoryProperty_fails_with_IAE_if_property_contains_an_empty_value() {
+ expectMissingPropertyIAE(this.randomPropertyName);
+
+ underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName);
+ }
+
+ @Test
+ @UseDataProvider("variousEmptyStrings")
+ public void addFromMandatoryProperty_adds_single_option_of_property_with_trimming(String emptyString) {
+ properties.put(randomPropertyName, emptyString + "-foo" + emptyString);
+
+ underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName);
+
+ assertThat(underTest.getAll()).containsOnly("-foo");
+ }
+
+ @Test
+ @UseDataProvider("variousEmptyStrings")
+ public void addFromMandatoryProperty_fails_with_MessageException_if_property_does_not_start_with_dash_after_trimmed(String emptyString) {
+ properties.put(randomPropertyName, emptyString + "foo -bar");
+
+ expectJvmOptionNotEmptyAndStartByDashMessageException(randomPropertyName, "foo");
+
+ underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName);
+ }
+
+ @Test
+ @UseDataProvider("variousEmptyStrings")
+ public void addFromMandatoryProperty_adds_options_of_property_with_trimming(String emptyString) {
+ properties.put(randomPropertyName, emptyString + "-foo" + emptyString + " -bar" + emptyString + " -duck" + emptyString);
+
+ underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName);
+
+ assertThat(underTest.getAll()).containsOnly("-foo", "-bar", "-duck");
+ }
+
+ @Test
+ public void addFromMandatoryProperty_supports_spaces_inside_options() {
+ properties.put(randomPropertyName, "-foo bar -duck");
+
+ underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName);
+
+ assertThat(underTest.getAll()).containsOnly("-foo bar", "-duck");
+ }
+
+ @Test
+ public void addFromMandatoryProperty_throws_IAE_if_option_starts_with_prefix_of_mandatory_option_but_has_different_value() {
+ String[] optionOverrides = {
+ randomPrefix,
+ randomPrefix + randomValue.substring(1),
+ randomPrefix + randomValue.substring(1),
+ randomPrefix + randomValue.substring(2),
+ randomPrefix + randomValue.substring(3),
+ randomPrefix + randomValue.substring(3) + randomAlphanumeric(1),
+ randomPrefix + randomValue.substring(3) + randomAlphanumeric(2),
+ randomPrefix + randomValue.substring(3) + randomAlphanumeric(3),
+ randomPrefix + randomValue + randomAlphanumeric(1)
+ };
+
+ JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue));
+
+ for (String optionOverride : optionOverrides) {
+ try {
+ properties.put(randomPropertyName, optionOverride);
+ underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName);
+ fail("an MessageException should have been thrown");
+ } catch (MessageException e) {
+ assertThat(e.getMessage())
+ .isEqualTo("a JVM option can't overwrite mandatory JVM options. " +
+ "The following JVM options defined by property '" + randomPropertyName + "' are invalid: " + optionOverride + " overwrites " + randomPrefix + randomValue);
+ }
+ }
+ }
+
+ @Test
+ public void addFromMandatoryProperty_checks_against_mandatory_options_is_case_sensitive() {
+ String[] optionOverrides = {
+ randomPrefix,
+ randomPrefix + randomValue.substring(1),
+ randomPrefix + randomValue.substring(1),
+ randomPrefix + randomValue.substring(2),
+ randomPrefix + randomValue.substring(3),
+ randomPrefix + randomValue.substring(3) + randomAlphanumeric(1),
+ randomPrefix + randomValue.substring(3) + randomAlphanumeric(2),
+ randomPrefix + randomValue.substring(3) + randomAlphanumeric(3),
+ randomPrefix + randomValue + randomAlphanumeric(1)
+ };
+
+ JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue));
+
+ for (String optionOverride : optionOverrides) {
+ properties.setProperty(randomPropertyName, optionOverride.toUpperCase(Locale.ENGLISH));
+ underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName);
+ }
+ }
+
+ @Test
+ public void addFromMandatoryProperty_reports_all_overriding_options_in_single_exception() {
+ String overriding1 = randomPrefix;
+ String overriding2 = randomPrefix + randomValue + randomAlphanumeric(1);
+ properties.setProperty(randomPropertyName, "-foo " + overriding1 + " -bar " + overriding2);
+
+ JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue));
+
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("a JVM option can't overwrite mandatory JVM options. " +
+ "The following JVM options defined by property '" + randomPropertyName + "' are invalid: " +
+ overriding1 + " overwrites " + randomPrefix + randomValue + ", " + overriding2 + " overwrites " + randomPrefix + randomValue);
+
+ underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName);
+ }
+
+ @Test
+ public void addFromMandatoryProperty_accepts_property_equal_to_mandatory_option_and_does_not_add_it_twice() {
+ JvmOptions underTest = new JvmOptions(ImmutableMap.of(randomPrefix, randomValue));
+
+ properties.put(randomPropertyName, randomPrefix + randomValue);
+ underTest.addFromMandatoryProperty(new Props(properties), randomPropertyName);
+
+ assertThat(underTest.getAll()).containsOnly(randomPrefix + randomValue);
+ }
+
+ @Test
+ public void toString_prints_all_jvm_options() {
+ underTest.add("-foo").add("-bar");
+
+ assertThat(underTest.toString()).isEqualTo("[-foo, -bar]");
+ }
+
+ private void expectJvmOptionNotNullNPE() {
+ expectedException.expect(NullPointerException.class);
+ expectedException.expectMessage("a JVM option can't be null");
+ }
+
+ private void expectJvmOptionNotEmptyAndStartByDashIAE() {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("a JVM option can't be empty and must start with '-'");
+ }
+
+ private void expectJvmOptionNotEmptyAndStartByDashMessageException(String randomPropertyName, String option) {
+ expectedException.expect(MessageException.class);
+ expectedException.expectMessage("a JVM option can't be empty and must start with '-'. " +
+ "The following JVM options defined by property '" + randomPropertyName + "' are invalid: " + option);
+ }
+
+ public void expectMissingPropertyIAE(String randomPropertyName) {
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Missing property: " + randomPropertyName);
+ }
+
+ @DataProvider()
+ public static Object[][] variousEmptyStrings() {
+ return new Object[][] {
+ {""},
+ {" "},
+ {" "}
+ };
+ }
+
+ private static Map<String, String> shuffleThenToMap(Stream<Option> stream) {
+ List<Option> options = stream.collect(Collectors.toList());
+ Collections.shuffle(options);
+ Map<String, String> res = new HashMap<>(options.size());
+ for (Option option : options) {
+ res.put(option.getPrefix(), option.getValue());
+ }
+ return res;
+ }
+
+ private static final class Option {
+ private final String prefix;
+ private final String value;
+
+ private Option(String prefix, String value) {
+ this.prefix = prefix;
+ this.value = value;
+ }
+
+ public String getPrefix() {
+ return prefix;
+ }
+
+ public String getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return "[" + prefix + "-" + value + ']';
+ }
+ }
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/command/WebJvmOptionsTest.java b/server/sonar-main/src/test/java/org/sonar/application/command/WebJvmOptionsTest.java
new file mode 100644
index 00000000000..dfd8f154e6a
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/command/WebJvmOptionsTest.java
@@ -0,0 +1,43 @@
+/*
+ * 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.command;
+
+import java.io.File;
+import java.io.IOException;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class WebJvmOptionsTest {
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void constructor_sets_mandatory_JVM_options() throws IOException {
+ File tmpDir = temporaryFolder.newFolder();
+ WebJvmOptions underTest = new WebJvmOptions(tmpDir);
+
+ assertThat(underTest.getAll()).containsExactly(
+ "-Djava.awt.headless=true", "-Dfile.encoding=UTF-8", "-Djava.io.tmpdir=" + tmpDir.getAbsolutePath());
+ }
+
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsFileSystemTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsFileSystemTest.java
new file mode 100644
index 00000000000..e015b6bcce3
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsFileSystemTest.java
@@ -0,0 +1,187 @@
+/*
+ * 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.es;
+
+import java.io.File;
+import java.io.IOException;
+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.ProcessProperties;
+import org.sonar.process.Props;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EsFileSystemTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void constructor_fails_with_IAE_if_sq_home_property_is_not_defined() {
+ Props props = new Props(new Properties());
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Property sonar.path.home is not set");
+
+ new EsFileSystem(props);
+ }
+
+ @Test
+ public void constructor_fails_with_IAE_if_temp_dir_property_is_not_defined() throws IOException {
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, temp.newFolder().getAbsolutePath());
+
+ expectedException.expect(IllegalArgumentException.class);
+ expectedException.expectMessage("Property sonar.path.temp is not set");
+
+ new EsFileSystem(props);
+ }
+
+ @Test
+ public void getHomeDirectory_is_elasticsearch_subdirectory_of_sq_home_directory() throws IOException {
+ File sqHomeDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, sqHomeDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+
+ EsFileSystem underTest = new EsFileSystem(props);
+
+ assertThat(underTest.getHomeDirectory()).isEqualTo(new File(sqHomeDir, "elasticsearch"));
+ }
+
+ @Test
+ public void getDataDirectory_is_data_es_subdirectory_of_sq_home_directory_by_default() throws IOException {
+ File sqHomeDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, sqHomeDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+
+ EsFileSystem underTest = new EsFileSystem(props);
+
+ assertThat(underTest.getDataDirectory()).isEqualTo(new File(sqHomeDir, "data/es"));
+ }
+
+ @Test
+ public void override_data_dir() throws Exception {
+ File sqHomeDir = temp.newFolder();
+ File tempDir = temp.newFolder();
+ File dataDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, sqHomeDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+
+ props.set(ProcessProperties.PATH_DATA, dataDir.getAbsolutePath());
+
+ EsFileSystem underTest = new EsFileSystem(props);
+
+ assertThat(underTest.getDataDirectory()).isEqualTo(new File(dataDir, "es"));
+ }
+
+ @Test
+ public void getLogDirectory_is_configured_with_non_nullable_PATH_LOG_variable() throws IOException {
+ File sqHomeDir = temp.newFolder();
+ File logDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, sqHomeDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, logDir.getAbsolutePath());
+
+ EsFileSystem underTest = new EsFileSystem(props);
+
+ assertThat(underTest.getLogDirectory()).isEqualTo(logDir);
+ }
+
+ @Test
+ public void conf_directory_is_conf_es_subdirectory_of_sq_temp_directory() throws IOException {
+ File tempDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+
+ EsFileSystem underTest = new EsFileSystem(props);
+
+ assertThat(underTest.getConfDirectory()).isEqualTo(new File(tempDir, "conf/es"));
+ }
+
+ @Test
+ public void getExecutable_resolve_executable_for_platform() throws IOException {
+ File sqHomeDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, sqHomeDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+
+ EsFileSystem underTest = new EsFileSystem(props);
+
+ if (System.getProperty("os.name").startsWith("Windows")) {
+ assertThat(underTest.getExecutable()).isEqualTo(new File(sqHomeDir, "elasticsearch/bin/elasticsearch.bat"));
+ } else {
+ assertThat(underTest.getExecutable()).isEqualTo(new File(sqHomeDir, "elasticsearch/bin/elasticsearch"));
+ }
+ }
+
+ @Test
+ public void getLog4j2Properties_is_in_es_conf_directory() throws IOException {
+ File tempDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+
+ EsFileSystem underTest = new EsFileSystem(props);
+
+ assertThat(underTest.getLog4j2Properties()).isEqualTo(new File(tempDir, "conf/es/log4j2.properties"));
+ }
+
+ @Test
+ public void getElasticsearchYml_is_in_es_conf_directory() throws IOException {
+ File tempDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+
+ EsFileSystem underTest = new EsFileSystem(props);
+
+ assertThat(underTest.getElasticsearchYml()).isEqualTo(new File(tempDir, "conf/es/elasticsearch.yml"));
+ }
+
+ @Test
+ public void getJvmOptions_is_in_es_conf_directory() throws IOException {
+ File tempDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, tempDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+
+ EsFileSystem underTest = new EsFileSystem(props);
+
+ assertThat(underTest.getJvmOptions()).isEqualTo(new File(tempDir, "conf/es/jvm.options"));
+ }
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsLoggingTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsLoggingTest.java
new file mode 100644
index 00000000000..7dc29ec2f55
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsLoggingTest.java
@@ -0,0 +1,131 @@
+/*
+ * 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.es;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashSet;
+import java.util.Properties;
+import java.util.Set;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.process.Props;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class EsLoggingTest {
+ @Rule
+ public TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ private EsLogging underTest = new EsLogging();
+
+ @Test
+ public void createProperties_with_empty_props() throws IOException {
+ File logDir = temporaryFolder.newFolder();
+ Properties properties = underTest.createProperties(newProps(), logDir);
+
+ verifyProperties(properties,
+ "status", "ERROR",
+ "appender.file_es.type", "RollingFile",
+ "appender.file_es.name", "file_es",
+ "appender.file_es.filePattern", new File(logDir, "es.%d{yyyy-MM-dd}.log").getAbsolutePath(),
+ "appender.file_es.fileName", new File(logDir, "es.log").getAbsolutePath(),
+ "appender.file_es.layout.type", "PatternLayout",
+ "appender.file_es.layout.pattern", "%d{yyyy.MM.dd HH:mm:ss} %-5level es[][%logger{1.}] %msg%n",
+ "appender.file_es.policies.type", "Policies",
+ "appender.file_es.policies.time.type", "TimeBasedTriggeringPolicy",
+ "appender.file_es.policies.time.interval", "1",
+ "appender.file_es.policies.time.modulate", "true",
+ "appender.file_es.strategy.type", "DefaultRolloverStrategy",
+ "appender.file_es.strategy.fileIndex", "nomax",
+ "appender.file_es.strategy.action.type", "Delete",
+ "appender.file_es.strategy.action.basepath", logDir.getAbsolutePath(),
+ "appender.file_es.strategy.action.maxDepth", "1",
+ "appender.file_es.strategy.action.condition.type", "IfFileName",
+ "appender.file_es.strategy.action.condition.glob", "es*",
+ "appender.file_es.strategy.action.condition.nested_condition.type", "IfAccumulatedFileCount",
+ "appender.file_es.strategy.action.condition.nested_condition.exceeds", "7",
+ "rootLogger.level", "INFO",
+ "rootLogger.appenderRef.file_es.ref", "file_es");
+ }
+
+ @Test
+ public void createProperties_sets_root_logger_to_INFO_if_no_property_is_set() throws IOException {
+ File logDir = temporaryFolder.newFolder();
+ Properties properties = underTest.createProperties(newProps(), logDir);
+
+ assertThat(properties.getProperty("rootLogger.level")).isEqualTo("INFO");
+ }
+
+ @Test
+ public void createProperties_sets_root_logger_to_global_property_if_set() throws IOException {
+ File logDir = temporaryFolder.newFolder();
+ Properties properties = underTest.createProperties(newProps("sonar.log.level", "TRACE"), logDir);
+
+ assertThat(properties.getProperty("rootLogger.level")).isEqualTo("TRACE");
+ }
+
+ @Test
+ public void createProperties_sets_root_logger_to_process_property_if_set() throws IOException {
+ File logDir = temporaryFolder.newFolder();
+ Properties properties = underTest.createProperties(newProps("sonar.log.level.es", "DEBUG"), logDir);
+
+ assertThat(properties.getProperty("rootLogger.level")).isEqualTo("DEBUG");
+ }
+
+ @Test
+ public void createProperties_sets_root_logger_to_process_property_over_global_property_if_both_set() throws IOException {
+ File logDir = temporaryFolder.newFolder();
+ Properties properties = underTest.createProperties(
+ newProps(
+ "sonar.log.level", "DEBUG",
+ "sonar.log.level.es", "TRACE"),
+ logDir);
+
+ assertThat(properties.getProperty("rootLogger.level")).isEqualTo("TRACE");
+ }
+
+ private static Props newProps(String... propertyKeysAndValues) {
+ assertThat(propertyKeysAndValues.length % 2).describedAs("Number of parameters must be even").isEqualTo(0);
+ Properties properties = new Properties();
+ for (int i = 0; i < propertyKeysAndValues.length; i++) {
+ properties.put(propertyKeysAndValues[i++], propertyKeysAndValues[i]);
+ }
+ return new Props(properties);
+ }
+
+ private void verifyProperties(Properties properties, String... expectedPropertyKeysAndValuesOrdered) {
+ if (expectedPropertyKeysAndValuesOrdered.length == 0) {
+ assertThat(properties.size()).isEqualTo(0);
+ } else {
+ assertThat(expectedPropertyKeysAndValuesOrdered.length % 2).describedAs("Number of parameters must be even").isEqualTo(0);
+ Set<String> keys = new HashSet<>(expectedPropertyKeysAndValuesOrdered.length / 2 + 1);
+ keys.add("status");
+ for (int i = 0; i < expectedPropertyKeysAndValuesOrdered.length; i++) {
+ String key = expectedPropertyKeysAndValuesOrdered[i++];
+ String value = expectedPropertyKeysAndValuesOrdered[i];
+ assertThat(properties.get(key)).describedAs("Unexpected value for property " + key).isEqualTo(value);
+ keys.add(key);
+ }
+ assertThat(properties.keySet()).containsOnly(keys.toArray());
+ }
+ }
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java
new file mode 100644
index 00000000000..1a09f0ac240
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsSettingsTest.java
@@ -0,0 +1,302 @@
+/*
+ * 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.es;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Random;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.rules.TemporaryFolder;
+import org.sonar.application.logging.ListAppender;
+import org.sonar.cluster.ClusterProperties;
+import org.sonar.process.ProcessProperties;
+import org.sonar.process.Props;
+import org.sonar.process.System2;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.cluster.ClusterProperties.CLUSTER_NAME;
+import static org.sonar.cluster.ClusterProperties.CLUSTER_SEARCH_HOSTS;
+
+public class EsSettingsTest {
+
+ private static final boolean CLUSTER_ENABLED = true;
+ private static final boolean CLUSTER_DISABLED = false;
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+ private ListAppender listAppender;
+
+ @After
+ public void tearDown() throws Exception {
+ if (listAppender != null) {
+ ListAppender.detachMemoryAppenderToLoggerOf(EsSettings.class, listAppender);
+ }
+ }
+
+ @Test
+ public void constructor_does_not_logs_warning_if_env_variable_ES_JVM_OPTIONS_is_not_set() {
+ this.listAppender = ListAppender.attachMemoryAppenderToLoggerOf(EsSettings.class);
+ Props props = minimalProps();
+ System2 system2 = mock(System2.class);
+ new EsSettings(props, new EsFileSystem(props), system2);
+
+ assertThat(listAppender.getLogs()).isEmpty();
+ }
+
+ @Test
+ public void constructor_does_not_logs_warning_if_env_variable_ES_JVM_OPTIONS_is_set_and_empty() {
+ this.listAppender = ListAppender.attachMemoryAppenderToLoggerOf(EsSettings.class);
+ Props props = minimalProps();
+ System2 system2 = mock(System2.class);
+ when(system2.getenv("ES_JVM_OPTIONS")).thenReturn(" ");
+ new EsSettings(props, new EsFileSystem(props), system2);
+
+ assertThat(listAppender.getLogs()).isEmpty();
+ }
+
+ @Test
+ public void constructor_logs_warning_if_env_variable_ES_JVM_OPTIONS_is_set_and_non_empty() throws IOException {
+ this.listAppender = ListAppender.attachMemoryAppenderToLoggerOf(EsSettings.class);
+ Props props = minimalProps();
+ System2 system2 = mock(System2.class);
+ when(system2.getenv("ES_JVM_OPTIONS")).thenReturn(randomAlphanumeric(2));
+ new EsSettings(props, new EsFileSystem(props), system2);
+
+ assertThat(listAppender.getLogs())
+ .extracting(ILoggingEvent::getMessage)
+ .containsOnly("ES_JVM_OPTIONS is defined but will be ignored. " +
+ "Use sonar.search.javaOpts and/or sonar.search.javaAdditionalOpts in sonar.properties to specify jvm options for Elasticsearch");
+ }
+
+ private Props minimalProps() {
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.PATH_HOME, randomAlphanumeric(12));
+ props.set(ProcessProperties.PATH_TEMP, randomAlphanumeric(12));
+ props.set(ProcessProperties.PATH_LOGS, randomAlphanumeric(12));
+ props.set(CLUSTER_NAME, randomAlphanumeric(12));
+ return props;
+ }
+
+ @Test
+ public void test_default_settings_for_standalone_mode() throws Exception {
+ File homeDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.SEARCH_PORT, "1234");
+ props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1");
+ props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+ props.set(CLUSTER_NAME, "sonarqube");
+
+ EsSettings esSettings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE);
+
+ Map<String, String> generated = esSettings.build();
+ assertThat(generated.get("transport.tcp.port")).isEqualTo("1234");
+ assertThat(generated.get("transport.host")).isEqualTo("127.0.0.1");
+
+ // no cluster, but cluster and node names are set though
+ assertThat(generated.get("cluster.name")).isEqualTo("sonarqube");
+ assertThat(generated.get("node.name")).isEqualTo("sonarqube");
+
+ assertThat(generated.get("path.data")).isNotNull();
+ assertThat(generated.get("path.logs")).isNotNull();
+ assertThat(generated.get("path.home")).isNull();
+ assertThat(generated.get("path.conf")).isNotNull();
+
+ // http is disabled for security reasons
+ assertThat(generated.get("http.enabled")).isEqualTo("false");
+
+ assertThat(generated.get("discovery.zen.ping.unicast.hosts")).isNull();
+ assertThat(generated.get("discovery.zen.minimum_master_nodes")).isEqualTo("1");
+ assertThat(generated.get("discovery.initial_state_timeout")).isEqualTo("30s");
+
+ assertThat(generated.get("action.auto_create_index")).isEqualTo("false");
+ }
+
+ @Test
+ public void test_default_settings_for_cluster_mode() throws Exception {
+ File homeDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ProcessProperties.SEARCH_PORT, "1234");
+ props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1");
+ props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+ props.set(ClusterProperties.CLUSTER_NAME, "sonarqube-1");
+ props.set(ClusterProperties.CLUSTER_ENABLED, "true");
+ props.set(ClusterProperties.CLUSTER_NODE_NAME, "node-1");
+
+ EsSettings esSettings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE);
+
+ Map<String, String> generated = esSettings.build();
+ assertThat(generated.get("cluster.name")).isEqualTo("sonarqube-1");
+ assertThat(generated.get("node.name")).isEqualTo("node-1");
+ }
+
+ @Test
+ public void test_node_name_default_for_cluster_mode() throws Exception {
+ File homeDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ClusterProperties.CLUSTER_NAME, "sonarqube");
+ props.set(ClusterProperties.CLUSTER_ENABLED, "true");
+ props.set(ProcessProperties.SEARCH_PORT, "1234");
+ props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1");
+ props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+ EsSettings esSettings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE);
+ Map<String, String> generated = esSettings.build();
+ assertThat(generated.get("node.name")).startsWith("sonarqube-");
+ }
+
+ @Test
+ public void test_node_name_default_for_standalone_mode() throws Exception {
+ File homeDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ props.set(ClusterProperties.CLUSTER_NAME, "sonarqube");
+ props.set(ClusterProperties.CLUSTER_ENABLED, "false");
+ props.set(ProcessProperties.SEARCH_PORT, "1234");
+ props.set(ProcessProperties.SEARCH_HOST, "127.0.0.1");
+ props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+ props.set(ProcessProperties.PATH_TEMP, temp.newFolder().getAbsolutePath());
+ props.set(ProcessProperties.PATH_LOGS, temp.newFolder().getAbsolutePath());
+ EsSettings esSettings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE);
+ Map<String, String> generated = esSettings.build();
+ assertThat(generated.get("node.name")).isEqualTo("sonarqube");
+ }
+
+ @Test
+ public void path_properties_are_values_from_EsFileSystem_argument() throws IOException {
+ EsFileSystem mockedEsFileSystem = mock(EsFileSystem.class);
+ when(mockedEsFileSystem.getHomeDirectory()).thenReturn(new File("/foo/home"));
+ when(mockedEsFileSystem.getConfDirectory()).thenReturn(new File("/foo/conf"));
+ when(mockedEsFileSystem.getLogDirectory()).thenReturn(new File("/foo/log"));
+ when(mockedEsFileSystem.getDataDirectory()).thenReturn(new File("/foo/data"));
+
+ EsSettings underTest = new EsSettings(minProps(new Random().nextBoolean()), mockedEsFileSystem, System2.INSTANCE);
+
+ Map<String, String> generated = underTest.build();
+ assertThat(generated.get("path.data")).isEqualTo("/foo/data");
+ assertThat(generated.get("path.logs")).isEqualTo("/foo/log");
+ assertThat(generated.get("path.conf")).isEqualTo("/foo/conf");
+ }
+
+ @Test
+ public void set_discovery_settings_if_cluster_is_enabled() throws Exception {
+ Props props = minProps(CLUSTER_ENABLED);
+ props.set(CLUSTER_SEARCH_HOSTS, "1.2.3.4:9000,1.2.3.5:8080");
+ Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build();
+
+ assertThat(settings.get("discovery.zen.ping.unicast.hosts")).isEqualTo("1.2.3.4:9000,1.2.3.5:8080");
+ assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("2");
+ assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("120s");
+ }
+
+ @Test
+ public void incorrect_values_of_minimum_master_nodes() throws Exception {
+ Props props = minProps(CLUSTER_ENABLED);
+ props.set(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, "ꝱꝲꝳପ");
+
+ EsSettings underTest = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE);
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Value of property sonar.search.minimumMasterNodes is not an integer:");
+ underTest.build();
+ }
+
+ @Test
+ public void cluster_is_enabled_with_defined_minimum_master_nodes() throws Exception {
+ Props props = minProps(CLUSTER_ENABLED);
+ props.set(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, "5");
+ Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build();
+
+ assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("5");
+ }
+
+ @Test
+ public void cluster_is_enabled_with_defined_initialTimeout() throws Exception {
+ Props props = minProps(CLUSTER_ENABLED);
+ props.set(ProcessProperties.SEARCH_INITIAL_STATE_TIMEOUT, "10s");
+ Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build();
+
+ assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("10s");
+ }
+
+ @Test
+ public void in_standalone_initialTimeout_is_not_overridable() throws Exception {
+ Props props = minProps(CLUSTER_DISABLED);
+ props.set(ProcessProperties.SEARCH_INITIAL_STATE_TIMEOUT, "10s");
+ Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build();
+
+ assertThat(settings.get("discovery.initial_state_timeout")).isEqualTo("30s");
+ }
+
+ @Test
+ public void in_standalone_minimumMasterNodes_is_not_overridable() throws Exception {
+ Props props = minProps(CLUSTER_DISABLED);
+ props.set(ProcessProperties.SEARCH_MINIMUM_MASTER_NODES, "5");
+ Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build();
+
+ assertThat(settings.get("discovery.zen.minimum_master_nodes")).isEqualTo("1");
+ }
+
+ @Test
+ public void enable_http_connector() throws Exception {
+ Props props = minProps(CLUSTER_DISABLED);
+ props.set(ProcessProperties.SEARCH_HTTP_PORT, "9010");
+ Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build();
+
+ assertThat(settings.get("http.port")).isEqualTo("9010");
+ assertThat(settings.get("http.host")).isEqualTo("127.0.0.1");
+ assertThat(settings.get("http.enabled")).isEqualTo("true");
+ }
+
+ @Test
+ public void enable_http_connector_different_host() throws Exception {
+ Props props = minProps(CLUSTER_DISABLED);
+ props.set(ProcessProperties.SEARCH_HTTP_PORT, "9010");
+ props.set(ProcessProperties.SEARCH_HOST, "127.0.0.2");
+ Map<String, String> settings = new EsSettings(props, new EsFileSystem(props), System2.INSTANCE).build();
+
+ assertThat(settings.get("http.port")).isEqualTo("9010");
+ assertThat(settings.get("http.host")).isEqualTo("127.0.0.2");
+ assertThat(settings.get("http.enabled")).isEqualTo("true");
+ }
+
+ private Props minProps(boolean cluster) throws IOException {
+ File homeDir = temp.newFolder();
+ Props props = new Props(new Properties());
+ ProcessProperties.completeDefaults(props);
+ props.set(ProcessProperties.PATH_HOME, homeDir.getAbsolutePath());
+ props.set(ClusterProperties.CLUSTER_ENABLED, Boolean.toString(cluster));
+ return props;
+ }
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/es/EsYmlSettingsTest.java b/server/sonar-main/src/test/java/org/sonar/application/es/EsYmlSettingsTest.java
new file mode 100644
index 00000000000..6920c7f2710
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/es/EsYmlSettingsTest.java
@@ -0,0 +1,62 @@
+package org.sonar.application.es;/*
+ * 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.
+ */
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+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;
+
+public class EsYmlSettingsTest {
+
+ @Rule
+ public TemporaryFolder folder= new TemporaryFolder();
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Test
+ public void test_generation_of_file() throws IOException {
+ File yamlFile = folder.newFile();
+ new EsYmlSettings(new HashMap<>()).writeToYmlSettingsFile(yamlFile);
+
+ assertThat(yamlFile).exists();
+ assertThat(yamlFile).hasContent("# This file has been automatically generated by SonarQube during startup.\n" +
+ "\n" +
+ "# DO NOT EDIT THIS FILE\n" +
+ "\n" +
+ "{\n" +
+ " }");
+ }
+
+ @Test
+ public void if_file_is_not_writable_ISE_must_be_thrown() throws IOException {
+ File yamlFile = folder.newFile();
+ yamlFile.setReadOnly();
+
+ expectedException.expect(IllegalStateException.class);
+ expectedException.expectMessage("Cannot write Elasticsearch yml settings file");
+
+ new EsYmlSettings(new HashMap<>()).writeToYmlSettingsFile(yamlFile);
+ }
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/logging/ListAppender.java b/server/sonar-main/src/test/java/org/sonar/application/logging/ListAppender.java
new file mode 100644
index 00000000000..7a8ca8bd56a
--- /dev/null
+++ b/server/sonar-main/src/test/java/org/sonar/application/logging/ListAppender.java
@@ -0,0 +1,53 @@
+/*
+ * 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.logging;
+
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.AppenderBase;
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.process.logging.LogbackHelper;
+
+public final class ListAppender extends AppenderBase<ILoggingEvent> {
+ private final List<ILoggingEvent> logs = new ArrayList<>();
+
+ @Override
+ protected void append(ILoggingEvent eventObject) {
+ logs.add(eventObject);
+ }
+
+ public List<ILoggingEvent> getLogs() {
+ return logs;
+ }
+
+ public static <T> ListAppender attachMemoryAppenderToLoggerOf(Class<T> loggerClass) {
+ ListAppender listAppender = new ListAppender();
+ new LogbackHelper().getRootContext().getLogger(loggerClass)
+ .addAppender(listAppender);
+ listAppender.start();
+ return listAppender;
+ }
+
+ public static <T> void detachMemoryAppenderToLoggerOf(Class<T> loggerClass, ListAppender listAppender) {
+ listAppender.stop();
+ new LogbackHelper().getRootContext().getLogger(loggerClass)
+ .detachAppender(listAppender);
+ }
+}
diff --git a/server/sonar-main/src/test/java/org/sonar/application/process/EsProcessMonitorTest.java b/server/sonar-main/src/test/java/org/sonar/application/process/EsProcessMonitorTest.java
index d0ef83444fc..7af20a08cc1 100644
--- a/server/sonar-main/src/test/java/org/sonar/application/process/EsProcessMonitorTest.java
+++ b/server/sonar-main/src/test/java/org/sonar/application/process/EsProcessMonitorTest.java
@@ -35,7 +35,7 @@ import org.elasticsearch.discovery.MasterNotDiscoveredException;
import org.junit.Test;
import org.slf4j.LoggerFactory;
import org.sonar.process.ProcessId;
-import org.sonar.process.command.EsCommand;
+import org.sonar.application.command.EsCommand;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.tuple;
diff --git a/server/sonar-main/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java b/server/sonar-main/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java
index a0a4bba391d..36a288c1f02 100644
--- a/server/sonar-main/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java
+++ b/server/sonar-main/src/test/java/org/sonar/application/process/ProcessLauncherImplTest.java
@@ -31,8 +31,8 @@ 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.application.command.JavaCommand;
+import org.sonar.application.command.JvmOptions;
import org.sonar.process.sharedmemoryfile.AllProcessesCommands;
import static org.assertj.core.api.Assertions.assertThat;