aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@sonarsource.com>2014-09-12 10:24:07 +0200
committerSimon Brandhof <simon.brandhof@sonarsource.com>2014-09-12 10:24:07 +0200
commit1fd2e682bfe5b6732e9097d0d3e57495da9e058c (patch)
tree765cfa78c98b9ae09828104cddcad0b1d5c2c253
parent85da787c6f0f5c47ec6e19681ed00fa8661f0fb8 (diff)
parentaeee283d2109c425828c96bc2952b947cce56566 (diff)
downloadsonarqube-1fd2e682bfe5b6732e9097d0d3e57495da9e058c.tar.gz
sonarqube-1fd2e682bfe5b6732e9097d0d3e57495da9e058c.zip
Merge branch 'process-monitoring' into branch-4.5
-rw-r--r--server/pom.xml3
-rw-r--r--server/process/sonar-process/pom.xml21
-rw-r--r--server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java36
-rw-r--r--server/process/sonar-process/src/test/java/org/sonar/process2/MonitorTest.java23
-rw-r--r--server/sonar-process-monitor/pom.xml87
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java175
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java124
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java35
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java208
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java60
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java98
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java133
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java69
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java78
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java103
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java70
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java23
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java38
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java42
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/InfiniteTerminationRmiConnector.java38
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java79
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java43
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java442
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java31
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java53
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java52
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt1
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt1
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt3
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt1
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml1
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties212
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties3
-rw-r--r--server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jarbin0 -> 854048 bytes
-rw-r--r--server/sonar-process/pom.xml108
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/AesCipher.java133
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java35
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/Cipher.java27
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java70
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/Encryption.java64
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java81
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java56
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java71
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/MessageException.java36
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java86
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java31
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java40
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java148
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java53
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java28
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java77
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/Props.java120
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/State.java26
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/StopperThread.java57
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/SystemExit.java52
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/Terminable.java28
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/package-info.java23
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java185
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java59
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java95
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java59
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java120
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java42
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java51
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java102
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java61
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java224
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java28
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/PropsTest.java135
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java56
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java116
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java81
-rw-r--r--server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt1
-rw-r--r--server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt1
-rw-r--r--server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt3
-rw-r--r--server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt1
-rw-r--r--server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml1
-rw-r--r--server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties212
-rw-r--r--server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties3
-rw-r--r--server/sonar-process/src/test/resources/sonar-dummy-app.jarbin0 -> 854048 bytes
-rw-r--r--server/sonar-process/test-jar-with-dependencies.xml19
-rw-r--r--server/sonar-search/src/main/java/org/sonar/search/SearchServer.java89
-rw-r--r--server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java6
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java79
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java29
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java1
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java6
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java8
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java38
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java3
-rw-r--r--server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java10
-rw-r--r--sonar-application/pom.xml5
-rw-r--r--sonar-application/src/main/java/org/sonar/application/App.java166
93 files changed, 5650 insertions, 281 deletions
diff --git a/server/pom.xml b/server/pom.xml
index c0ec6370c42..55ee0ba06c5 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -11,7 +11,8 @@
<name>SonarQube :: Server :: Parent</name>
<modules>
- <module>process</module>
+ <module>sonar-process</module>
+ <module>sonar-process-monitor</module>
<module>sonar-search</module>
<module>sonar-server</module>
<module>sonar-web</module>
diff --git a/server/process/sonar-process/pom.xml b/server/process/sonar-process/pom.xml
index a0e76e6c864..48e39182b18 100644
--- a/server/process/sonar-process/pom.xml
+++ b/server/process/sonar-process/pom.xml
@@ -67,36 +67,19 @@
<scope>test</scope>
</dependency>
</dependencies>
- <!--
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-dependency-plugin</artifactId>
- <version>2.8</version>
+ <artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
- <id>copy</id>
- <phase>process-test-resources</phase>
<goals>
- <goal>copy</goal>
+ <goal>test-jar</goal>
</goals>
- <configuration>
- <artifactItems>
- <artifactItem>
- <groupId>org.codehaus.sonar</groupId>
- <artifactId>sonar-dummy-app</artifactId>
- <version>${project.version}</version>
- <type>jar</type>
- <outputDirectory>${project.build.testOutputDirectory}</outputDirectory>
- <destFileName>sonar-dummy-app.jar</destFileName>
- </artifactItem>
- </artifactItems>
- </configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
- -->
</project>
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java b/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java
index f399318aa1e..e47c24682d4 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java
+++ b/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java
@@ -35,27 +35,37 @@ public class ProcessUtils {
// only static stuff
}
+ /**
+ * Do not abuse to this method. It uses exceptions to get status.
+ * @return false if process is null or terminated, else true.
+ */
public static boolean isAlive(@Nullable Process process) {
- if (process == null) {
- return false;
- }
- try {
- process.exitValue();
- return false;
- } catch (IllegalThreadStateException e) {
- LOGGER.trace("Process has no exit value yet", e);
- return true;
+ boolean alive = false;
+ if (process != null) {
+ try {
+ process.exitValue();
+ } catch (IllegalThreadStateException ignored) {
+ alive = true;
+ }
}
+ return alive;
}
- public static void destroyQuietly(@Nullable Process process) {
- if (process != null && isAlive(process)) {
+ /**
+ * Destroys process (equivalent to kill -9) if alive
+ * @return true if the process was destroyed, false if process is null or already destroyed.
+ */
+ public static boolean destroyQuietly(@Nullable Process process) {
+ boolean destroyed = false;
+ if (isAlive(process)) {
try {
process.destroy();
- } catch (Exception ignored) {
- LOGGER.warn("Exception while destroying the process", ignored);
+ destroyed = true;
+ } catch (Exception e) {
+ LoggerFactory.getLogger(ProcessUtils.class).error("Fail to destroy " + process);
}
}
+ return destroyed;
}
public static void addSelfShutdownHook(final Terminable terminable) {
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process2/MonitorTest.java b/server/process/sonar-process/src/test/java/org/sonar/process2/MonitorTest.java
new file mode 100644
index 00000000000..baaed0fe374
--- /dev/null
+++ b/server/process/sonar-process/src/test/java/org/sonar/process2/MonitorTest.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process2;
+
+public class MonitorTest {
+}
diff --git a/server/sonar-process-monitor/pom.xml b/server/sonar-process-monitor/pom.xml
new file mode 100644
index 00000000000..fa0869a52b8
--- /dev/null
+++ b/server/sonar-process-monitor/pom.xml
@@ -0,0 +1,87 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>server</artifactId>
+ <version>4.5-SNAPSHOT</version>
+ <relativePath>../</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>sonar-process-monitor</artifactId>
+ <name>SonarQube :: Process Monitor</name>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-process</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>commons-lang</groupId>
+ <artifactId>commons-lang</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.easytesting</groupId>
+ <artifactId>fest-assert</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-process</artifactId>
+ <type>test-jar</type>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.github.kevinsawicki</groupId>
+ <artifactId>http-request</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+</project>
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java
new file mode 100644
index 00000000000..dd564d77247
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java
@@ -0,0 +1,175 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import javax.annotation.Nullable;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+
+public class JavaCommand {
+
+ // unique key among the group of commands to launch
+ private final String key;
+
+ private File workDir;
+
+ // any available port by default
+ private int jmxPort = -1;
+
+ // for example -Xmx1G
+ private final List<String> javaOptions = new ArrayList<String>();
+
+ // entry point
+ private String className;
+
+ // relative path to JAR files
+ private final List<String> classpath = new ArrayList<String>();
+
+ // program arguments (parameters of main(String[])
+ private final Map<String, String> arguments = new LinkedHashMap<String, String>();
+
+ private final Map<String, String> envVariables = new HashMap<String, String>(System.getenv());
+
+ public JavaCommand(String key) {
+ this.key = key;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ public File getWorkDir() {
+ return workDir;
+ }
+
+ public JavaCommand setWorkDir(File workDir) {
+ this.workDir = workDir;
+ return this;
+ }
+
+ public JavaCommand setTempDir(File tempDir) {
+ this.javaOptions.add("-Djava.io.tmpdir=" + tempDir.getAbsolutePath());
+ return this;
+ }
+
+ public int getJmxPort() {
+ return jmxPort;
+ }
+
+ public JavaCommand setJmxPort(int jmxPort) {
+ this.jmxPort = jmxPort;
+ return this;
+ }
+
+ public List<String> getJavaOptions() {
+ return javaOptions;
+ }
+
+ public JavaCommand addJavaOption(String s) {
+ javaOptions.add(s);
+ return this;
+ }
+
+ public JavaCommand addJavaOptions(String s) {
+ Collections.addAll(javaOptions, s.split(" "));
+ return this;
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ public JavaCommand setClassName(String className) {
+ this.className = className;
+ return this;
+ }
+
+ public List<String> getClasspath() {
+ return classpath;
+ }
+
+ public JavaCommand addClasspath(String s) {
+ classpath.add(s);
+ return this;
+ }
+
+ public Map<String, String> getArguments() {
+ return arguments;
+ }
+
+ public JavaCommand setArgument(String key, @Nullable String value) {
+ if (value == null) {
+ arguments.remove(key);
+ } else {
+ arguments.put(key, value);
+ }
+ return this;
+ }
+
+ public JavaCommand setArguments(Properties args) {
+ for (Map.Entry<Object, Object> entry : args.entrySet()) {
+ setArgument(entry.getKey().toString(), entry.getValue() != null ? entry.getValue().toString() : null);
+ }
+ return this;
+ }
+
+ public Map<String, String> getEnvVariables() {
+ return envVariables;
+ }
+
+ public JavaCommand setEnvVariable(String key, @Nullable String value) {
+ if (value == null) {
+ envVariables.remove(key);
+ } else {
+ envVariables.put(key, value);
+ }
+ return this;
+ }
+
+ public boolean isDebugMode() {
+ for (String javaOption : javaOptions) {
+ if (javaOption.contains("-agentlib:jdwp")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("JavaCommand{");
+ sb.append("workDir=").append(workDir);
+ sb.append(", jmxPort=").append(jmxPort);
+ sb.append(", javaOptions=").append(javaOptions);
+ sb.append(", className='").append(className).append('\'');
+ sb.append(", classpath=").append(classpath);
+ sb.append(", arguments=").append(arguments);
+ sb.append(", envVariables=").append(envVariables);
+ sb.append('}');
+ return sb.toString();
+ }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java
new file mode 100644
index 00000000000..ff8ef88e8f7
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java
@@ -0,0 +1,124 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.LoggerFactory;
+import org.sonar.process.LoopbackAddress;
+import org.sonar.process.ProcessEntryPoint;
+import org.sonar.process.ProcessUtils;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Properties;
+
+public class JavaProcessLauncher {
+
+ private final Timeouts timeouts;
+
+ public JavaProcessLauncher(Timeouts timeouts) {
+ this.timeouts = timeouts;
+ }
+
+ ProcessRef launch(JavaCommand command) {
+ Process process = null;
+ try {
+ ProcessBuilder processBuilder = create(command);
+ LoggerFactory.getLogger(getClass()).info("Launch {}: {}",
+ command.getKey(), StringUtils.join(processBuilder.command(), " "));
+ process = processBuilder.start();
+ StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), command.getKey());
+ StreamGobbler inputGobbler = new StreamGobbler(process.getInputStream(), command.getKey());
+ inputGobbler.start();
+ errorGobbler.start();
+
+ return new ProcessRef(command.getKey(), process, errorGobbler, inputGobbler);
+
+ } catch (Exception e) {
+ // just in case
+ ProcessUtils.destroyQuietly(process);
+ throw new IllegalStateException("Fail to launch " + command.getKey(), e);
+ }
+ }
+
+ private ProcessBuilder create(JavaCommand javaCommand) {
+ List<String> commands = new ArrayList<String>();
+ commands.add(buildJavaPath());
+ commands.addAll(javaCommand.getJavaOptions());
+ commands.addAll(buildJmxOptions(javaCommand));
+ commands.addAll(buildClasspath(javaCommand));
+ commands.add(javaCommand.getClassName());
+
+ // TODO warning - does it work if temp dir contains a whitespace ?
+ commands.add(buildPropertiesFile(javaCommand).getAbsolutePath());
+
+ ProcessBuilder processBuilder = new ProcessBuilder();
+ processBuilder.command(commands);
+ processBuilder.directory(javaCommand.getWorkDir());
+ processBuilder.environment().putAll(javaCommand.getEnvVariables());
+ return processBuilder;
+ }
+
+ private String buildJavaPath() {
+ String separator = System.getProperty("file.separator");
+ return new File(new File(System.getProperty("java.home")),
+ "bin" + separator + "java").getAbsolutePath();
+ }
+
+ private List<String> buildJmxOptions(JavaCommand javaCommand) {
+ if (javaCommand.getJmxPort() < 1) {
+ throw new IllegalStateException("JMX port is not set");
+ }
+ return Arrays.asList(
+ "-Dcom.sun.management.jmxremote",
+ "-Dcom.sun.management.jmxremote.port=" + javaCommand.getJmxPort(),
+ "-Dcom.sun.management.jmxremote.authenticate=false",
+ "-Dcom.sun.management.jmxremote.ssl=false",
+ "-Djava.rmi.server.hostname=" + LoopbackAddress.get().getHostAddress());
+ }
+
+ private List<String> buildClasspath(JavaCommand javaCommand) {
+ return Arrays.asList("-cp", StringUtils.join(javaCommand.getClasspath(), System.getProperty("path.separator")));
+ }
+
+ private File buildPropertiesFile(JavaCommand javaCommand) {
+ File propertiesFile = null;
+ try {
+ propertiesFile = File.createTempFile("sq-conf", "properties");
+ Properties props = new Properties();
+ props.putAll(javaCommand.getArguments());
+ props.setProperty(ProcessEntryPoint.PROPERTY_PROCESS_KEY, javaCommand.getKey());
+ props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, String.valueOf(javaCommand.isDebugMode()));
+ props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_TIMEOUT, String.valueOf(timeouts.getAutokillPingTimeout()));
+ props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_INTERVAL, String.valueOf(timeouts.getAutokillPingInterval()));
+ props.setProperty(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, String.valueOf(timeouts.getTerminationTimeout()));
+ OutputStream out = new FileOutputStream(propertiesFile);
+ props.store(out, String.format("Temporary properties file for command [%s]", javaCommand.getKey()));
+ out.close();
+ return propertiesFile;
+ } catch (Exception e) {
+ throw new IllegalStateException("Cannot write temporary settings to " + propertiesFile, e);
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java
new file mode 100644
index 00000000000..b06ea684d41
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+/**
+ * Interactions with monitored process
+ */
+public interface JmxConnector {
+
+ void connect(JavaCommand command, ProcessRef processRef);
+
+ void ping(ProcessRef process);
+
+ boolean isReady(ProcessRef process);
+
+ void terminate(ProcessRef process);
+
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java
new file mode 100644
index 00000000000..ccf2af343dc
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java
@@ -0,0 +1,208 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.slf4j.LoggerFactory;
+import org.sonar.process.Lifecycle;
+import org.sonar.process.MessageException;
+import org.sonar.process.State;
+import org.sonar.process.SystemExit;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+public class Monitor {
+
+ private final List<ProcessRef> processes = new CopyOnWriteArrayList<ProcessRef>();
+ private final TerminatorThread terminator;
+ private final JavaProcessLauncher launcher;
+ private final JmxConnector jmxConnector;
+ private final Lifecycle lifecycle = new Lifecycle();
+ private final Timeouts timeouts;
+
+ private final SystemExit systemExit;
+ private Thread shutdownHook = new Thread(new MonitorShutdownHook(), "Monitor Shutdown Hook");
+
+ // used by awaitTermination() to block until all processes are shutdown
+ private final List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<WatcherThread>();
+
+ Monitor(JavaProcessLauncher launcher, JmxConnector jmxConnector, Timeouts timeouts, SystemExit exit) {
+ this.launcher = launcher;
+ this.jmxConnector = jmxConnector;
+ this.timeouts = timeouts;
+ this.terminator = new TerminatorThread(processes, jmxConnector, timeouts);
+ this.systemExit = exit;
+ }
+
+ public static Monitor create() {
+ Timeouts timeouts = new Timeouts();
+ return new Monitor(new JavaProcessLauncher(timeouts), new RmiJmxConnector(timeouts),
+ timeouts, new SystemExit());
+ }
+
+ /**
+ * Starts commands and blocks current thread until all processes are in state {@link State#STARTED}.
+ * @throws java.lang.IllegalArgumentException if commands list is empty
+ * @throws java.lang.IllegalStateException if already started or if at least one process failed to start. In this case
+ * all processes are terminated. No need to execute {@link #stop()}
+ */
+ public void start(List<JavaCommand> commands) {
+ if (commands.isEmpty()) {
+ throw new IllegalArgumentException("At least one command is required");
+ }
+
+ if (!lifecycle.tryToMoveTo(State.STARTING)) {
+ throw new IllegalStateException("Can not start multiple times");
+ }
+
+ // intercepts CTRL-C
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+
+ for (JavaCommand command : commands) {
+ try {
+ ProcessRef processRef = launcher.launch(command);
+ monitor(command, processRef);
+ } catch (RuntimeException e) {
+ // fail to start or to monitor
+ stop();
+ throw e;
+ }
+ }
+
+ if (!lifecycle.tryToMoveTo(State.STARTED)) {
+ // stopping or stopped during startup, for instance :
+ // 1. A is started
+ // 2. B starts
+ // 3. A crashes while B is starting
+ // 4. if B was not monitored during Terminator execution, then it's an alive orphan
+ stop();
+ throw new IllegalStateException("Stopped during startup");
+ }
+ }
+
+ private void monitor(JavaCommand command, ProcessRef processRef) {
+ // physically watch if process is alive
+ WatcherThread watcherThread = new WatcherThread(processRef, this);
+ watcherThread.start();
+ watcherThreads.add(watcherThread);
+
+ // add to list of monitored processes only when successfully connected to it
+ jmxConnector.connect(command, processRef);
+ processes.add(processRef);
+
+ // ping process on a regular basis
+ processRef.setPingEnabled(!command.isDebugMode());
+ if (processRef.isPingEnabled()) {
+ PingerThread.startPinging(processRef, jmxConnector, timeouts);
+ }
+
+ // wait for process to be ready (accept requests or so on)
+ waitForReady(processRef);
+
+ LoggerFactory.getLogger(getClass()).info(String.format("%s is up", processRef));
+ }
+
+ private void waitForReady(ProcessRef processRef) {
+ boolean ready = false;
+ while (!ready) {
+ if (processRef.isTerminated()) {
+ throw new MessageException(String.format("%s failed to start", processRef));
+ }
+ try {
+ ready = jmxConnector.isReady(processRef);
+ } catch (Exception ignored) {
+ // pb with the JMX connection, can occur if RMI not initialized yet
+ }
+ try {
+ Thread.sleep(300L);
+ } catch (InterruptedException e) {
+ throw new IllegalStateException("Interrupted while waiting for " + processRef + " to be ready", e);
+ }
+ }
+ }
+
+ /**
+ * Blocks until all processes are terminated
+ */
+ public void awaitTermination() {
+ for (WatcherThread watcherThread : watcherThreads) {
+ while (watcherThread.isAlive()) {
+ try {
+ watcherThread.join();
+ } catch (InterruptedException ignored) {
+ // ignore, stop blocking
+ }
+ }
+ }
+ }
+
+ /**
+ * Blocks until all processes are terminated.
+ */
+ public void stop() {
+ terminateAsync();
+ try {
+ terminator.join();
+ } catch (InterruptedException ignored) {
+ // ignore, stop blocking
+ }
+ // safeguard if TerminatorThread is buggy
+ hardKillAll();
+ lifecycle.tryToMoveTo(State.STOPPED);
+ systemExit.exit(0);
+ }
+
+ /**
+ * Asks for processes termination and returns without blocking until termination.
+ * @return true if termination was requested, false if it was already being terminated
+ */
+ boolean terminateAsync() {
+ boolean requested = false;
+ if (lifecycle.tryToMoveTo(State.STOPPING)) {
+ requested = true;
+ terminator.start();
+ }
+ return requested;
+ }
+
+ private void hardKillAll() {
+ // no specific order, kill'em all!!!
+ for (ProcessRef process : processes) {
+ process.hardKill();
+ }
+ }
+
+ public State getState() {
+ return lifecycle.getState();
+ }
+
+ Thread getShutdownHook() {
+ return shutdownHook;
+ }
+
+ private class MonitorShutdownHook implements Runnable {
+ @Override
+ public void run() {
+ systemExit.setInShutdownHook();
+ // blocks until everything is corrected terminated
+ stop();
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java
new file mode 100644
index 00000000000..65df8546e33
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java
@@ -0,0 +1,60 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * This thread pings a process - through RMI - at fixed delay
+ */
+class PingerThread extends Thread {
+
+ private final ProcessRef processRef;
+ private final JmxConnector jmxConnector;
+
+ private PingerThread(ProcessRef process, JmxConnector jmxConnector) {
+ // it's important to give a name for traceability in profiling tools like visualVM
+ super(String.format("Ping[%s]", process.getKey()));
+ setDaemon(true);
+ this.processRef = process;
+ this.jmxConnector = jmxConnector;
+ }
+
+ @Override
+ public void run() {
+ if (!processRef.isTerminated() && processRef.isPingEnabled()) {
+ try {
+ jmxConnector.ping(processRef);
+ } catch (Exception ignored) {
+ // failed to ping
+ }
+ } else {
+ interrupt();
+ }
+ }
+
+ static void startPinging(ProcessRef processRef, JmxConnector jmxConnector, Timeouts timeouts) {
+ ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+ PingerThread pinger = new PingerThread(processRef, jmxConnector);
+ scheduler.scheduleAtFixedRate(pinger, 0L, timeouts.getMonitorPingInterval(), TimeUnit.MILLISECONDS);
+ }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java
new file mode 100644
index 00000000000..8d926c39d72
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java
@@ -0,0 +1,98 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.sonar.process.ProcessUtils;
+
+class ProcessRef {
+
+ private final String key;
+ private final Process process;
+ private final StreamGobbler[] gobblers;
+ private volatile boolean terminated = false;
+ private volatile boolean pingEnabled = true;
+
+ ProcessRef(String key, Process process, StreamGobbler... gobblers) {
+ this.key = key;
+ this.process = process;
+ this.terminated = !ProcessUtils.isAlive(process);
+ this.gobblers = gobblers;
+ }
+
+ /**
+ * Unique logical key (not the pid), for instance "ES"
+ */
+ String getKey() {
+ return key;
+ }
+
+ /**
+ * The {@link java.lang.Process}
+ */
+ Process getProcess() {
+ return process;
+ }
+
+ /**
+ * Almost real-time status
+ */
+ boolean isTerminated() {
+ return terminated;
+ }
+
+ /**
+ * Sending pings can be disabled when requesting for termination or when process is on debug mode (JDWP)
+ */
+ void setPingEnabled(boolean b) {
+ this.pingEnabled = b;
+ }
+
+ boolean isPingEnabled() {
+ return pingEnabled;
+ }
+
+ /**
+ * Destroy the process without gracefully asking it to terminate (kill -9).
+ * @return true if the process was killed, false if process is already terminated
+ */
+ boolean hardKill() {
+ boolean killed = false;
+ terminated = true;
+ pingEnabled = false;
+ if (ProcessUtils.isAlive(process)) {
+ ProcessUtils.destroyQuietly(process);
+ killed = true;
+ }
+ for (StreamGobbler gobbler : gobblers) {
+ StreamGobbler.waitUntilFinish(gobbler);
+ }
+ ProcessUtils.closeStreams(process);
+ return killed;
+ }
+
+ void setTerminated(boolean b) {
+ this.terminated = b;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Process[%s]", key);
+ }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java
new file mode 100644
index 00000000000..d8889d9c45e
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java
@@ -0,0 +1,133 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.sonar.process.JmxUtils;
+import org.sonar.process.LoopbackAddress;
+import org.sonar.process.ProcessMXBean;
+import org.sonar.process.ProcessUtils;
+
+import javax.annotation.CheckForNull;
+import javax.management.JMX;
+import javax.management.MBeanServerConnection;
+import javax.management.remote.JMXConnector;
+import javax.management.remote.JMXConnectorFactory;
+import javax.management.remote.JMXServiceURL;
+
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+class RmiJmxConnector implements JmxConnector {
+
+ static {
+ /*
+ Prevents such warnings :
+
+ WARNING: Failed to restart: java.io.IOException: Failed to get a RMI stub: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
+ java.net.ConnectException: Connection refused]
+ Sep 11, 2014 7:32:32 PM RMIConnector RMIClientCommunicatorAdmin-doStop
+ WARNING: Failed to call the method close():java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
+ java.net.ConnectException: Connection refused
+ Sep 11, 2014 7:32:32 PM ClientCommunicatorAdmin Checker-run
+ WARNING: Failed to check connection: java.net.ConnectException: Connection refused
+ Sep 11, 2014 7:32:32 PM ClientCommunicatorAdmin Checker-run
+ WARNING: stopping
+ */
+ System.setProperty("sun.rmi.transport.tcp.logLevel", "SEVERE");
+ }
+
+ private final Map<ProcessRef, ProcessMXBean> mbeans = new IdentityHashMap<ProcessRef, ProcessMXBean>();
+ private final Timeouts timeouts;
+
+ RmiJmxConnector(Timeouts timeouts) {
+ this.timeouts = timeouts;
+ }
+
+ @Override
+ public synchronized void connect(final JavaCommand command, ProcessRef processRef) {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ ConnectorCallable callable = new ConnectorCallable(command, processRef.getProcess());
+ try {
+ Future<ProcessMXBean> future = executor.submit(callable);
+ ProcessMXBean mxBean = future.get(timeouts.getJmxConnectionTimeout(), TimeUnit.MILLISECONDS);
+ if (mxBean != null) {
+ mbeans.put(processRef, mxBean);
+ }
+ } catch (Exception e) {
+ if (callable.latestException != null) {
+ throw callable.latestException;
+ }
+ throw new IllegalStateException("Fail to connect to JMX", e);
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ @Override
+ public void ping(ProcessRef processRef) {
+ mbeans.get(processRef).ping();
+ }
+
+ @Override
+ public boolean isReady(ProcessRef processRef) {
+ return mbeans.get(processRef).isReady();
+ }
+
+ @Override
+ public void terminate(ProcessRef processRef) {
+ mbeans.get(processRef).terminate();
+ }
+
+ private static class ConnectorCallable implements Callable<ProcessMXBean> {
+ private final JavaCommand command;
+ private final Process process;
+ private RuntimeException latestException;
+
+ private ConnectorCallable(JavaCommand command, Process process) {
+ this.command = command;
+ this.process = process;
+ }
+
+ @Override
+ @CheckForNull
+ public ProcessMXBean call() throws Exception {
+ JMXServiceURL jmxUrl = JmxUtils.serviceUrl(LoopbackAddress.get(), command.getJmxPort());
+ while (ProcessUtils.isAlive(process)) {
+ try {
+ JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, null);
+ MBeanServerConnection mBeanServer = jmxConnector.getMBeanServerConnection();
+ return JMX.newMBeanProxy(mBeanServer, JmxUtils.objectName(command.getKey()), ProcessMXBean.class);
+ } catch (Exception e) {
+ latestException = new IllegalStateException(String.format(
+ "Fail to connect to JMX bean of %s [%s] ", command.getKey(), jmxUrl), e);
+ }
+ Thread.sleep(300L);
+ }
+
+ // process went down, no need to connect
+ return null;
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java
new file mode 100644
index 00000000000..55d95c8a467
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java
@@ -0,0 +1,69 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+
+import java.io.BufferedReader;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+/**
+ * Reads process output and writes to logs
+ */
+class StreamGobbler extends Thread {
+
+ private final InputStream is;
+ private final Logger logger;
+
+ StreamGobbler(InputStream is, String processKey) {
+ super(String.format("Gobbler[%s]", processKey));
+ this.is = is;
+ this.logger = LoggerFactory.getLogger(processKey);
+ }
+
+ @Override
+ public void run() {
+ BufferedReader br = new BufferedReader(new InputStreamReader(is));
+ try {
+ String line;
+ while ((line = br.readLine()) != null) {
+ logger.info(line);
+ }
+ } catch (Exception ignored) {
+
+ } finally {
+ IOUtils.closeQuietly(br);
+ }
+ }
+
+ static void waitUntilFinish(@Nullable StreamGobbler gobbler) {
+ if (gobbler != null) {
+ try {
+ gobbler.join();
+ } catch (InterruptedException ignored) {
+ }
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java
new file mode 100644
index 00000000000..775a036bb2f
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java
@@ -0,0 +1,78 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.slf4j.LoggerFactory;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Terminates all monitored processes. Tries to gracefully terminate each process,
+ * then kill if timeout expires. Ping monitoring is disabled so process auto kills (self graceful termination, else self kill)
+ * if it does not receive the termination request.
+ */
+class TerminatorThread extends Thread {
+
+ private final List<ProcessRef> processes;
+ private final JmxConnector jmxConnector;
+ private final Timeouts timeouts;
+
+ TerminatorThread(List<ProcessRef> processes, JmxConnector jmxConnector, Timeouts timeouts) {
+ super("Terminator");
+ this.processes = processes;
+ this.jmxConnector = jmxConnector;
+ this.timeouts = timeouts;
+ }
+
+ @Override
+ public void run() {
+ // terminate in reverse order of startup (dependency order)
+ for (int index = processes.size() - 1; index >= 0; index--) {
+ final ProcessRef processRef = processes.get(index);
+ if (!processRef.isTerminated()) {
+ processRef.setPingEnabled(false);
+
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ Future future = executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ // ask for graceful termination
+ LoggerFactory.getLogger(getClass()).info("Request termination of " + processRef);
+ jmxConnector.terminate(processRef);
+ }
+ });
+ try {
+ future.get(timeouts.getTerminationTimeout(), TimeUnit.MILLISECONDS);
+ } catch (Exception ignored) {
+ // failed to gracefully stop in a timely fashion
+ LoggerFactory.getLogger(getClass()).info(String.format("Kill %s", processRef));
+ } finally {
+ executor.shutdownNow();
+ // kill even if graceful termination was done, just to be sure that physical process is really down
+ processRef.hardKill();
+ }
+ }
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java
new file mode 100644
index 00000000000..30ab3f5ce0c
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java
@@ -0,0 +1,103 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+/**
+ * Most of the timeouts involved in process monitoring, in milliseconds
+ */
+class Timeouts {
+
+ private long terminationTimeout = 120000L;
+ private long jmxConnectionTimeout = 30000L;
+ private long monitorPingInterval = 3000L;
+ private long autokillPingTimeout = 60000L;
+ private long autokillPingInterval = 3000L;
+
+ /**
+ * [monitor] Timeout to get connected to RMI MXBean while process is alive
+ */
+ long getJmxConnectionTimeout() {
+ return jmxConnectionTimeout;
+ }
+
+ /**
+ * @see #getJmxConnectionTimeout()
+ */
+ void setJmxConnectionTimeout(long l) {
+ this.jmxConnectionTimeout = l;
+ }
+
+ /**
+ * [monitor] Delay between each ping request
+ */
+ long getMonitorPingInterval() {
+ return monitorPingInterval;
+ }
+
+ /**
+ * @see #getMonitorPingInterval()
+ */
+ void setMonitorPingInterval(long l) {
+ this.monitorPingInterval = l;
+ }
+
+ /**
+ * [monitored process] maximum age of last received ping before process autokills
+ */
+ long getAutokillPingTimeout() {
+ return autokillPingTimeout;
+ }
+
+ /**
+ * @see #getAutokillPingTimeout()
+ */
+ void setAutokillPingTimeout(long l) {
+ this.autokillPingTimeout = l;
+ }
+
+ /**
+ * [monitored process] delay between checks of freshness of received pings
+ */
+ long getAutokillPingInterval() {
+ return autokillPingInterval;
+ }
+
+ /**
+ * @see #getAutokillPingInterval()
+ */
+ void setAutokillPingInterval(long l) {
+ this.autokillPingInterval = l;
+ }
+
+ /**
+ * [both monitor and monitored process] timeout of graceful termination before hard killing
+ */
+ long getTerminationTimeout() {
+ return terminationTimeout;
+ }
+
+ /**
+ * @see #getTerminationTimeout()
+ */
+ void setTerminationTimeout(long l) {
+ this.terminationTimeout = l;
+ }
+
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java
new file mode 100644
index 00000000000..a0b92f5ddc8
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java
@@ -0,0 +1,70 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.slf4j.LoggerFactory;
+import org.sonar.process.ProcessUtils;
+
+/**
+ * This thread blocks as long as the monitored process is physically alive.
+ * It avoids from executing {@link Process#exitValue()} at a fixed rate :
+ * <ul>
+ * <li>no usage of exception for flow control. Indeed {@link Process#exitValue()} throws an exception
+ * if process is alive. There's no method <code>Process#isAlive()</code></li>
+ * <li>no delay, instantaneous notification that process is down</li>
+ * </ul>
+ */
+class WatcherThread extends Thread {
+
+ private final ProcessRef process;
+ private final Monitor monitor;
+
+ WatcherThread(ProcessRef processRef, Monitor monitor) {
+ // this name is different than Thread#toString(), which includes name, priority
+ // and thread group
+ // -> do not override toString()
+ super(String.format("Watch[%s]", processRef.getKey()));
+ this.process = processRef;
+ this.monitor = monitor;
+ }
+
+ @Override
+ public void run() {
+ boolean alive = true;
+ while (alive) {
+ try {
+ process.getProcess().waitFor();
+ process.setTerminated(true);
+ LoggerFactory.getLogger(getClass()).info(process + " is down");
+ // terminate all other processes, but in another thread
+ monitor.stop();
+ alive = false;
+ } catch (InterruptedException ignored) {
+ if (ProcessUtils.isAlive(process.getProcess())) {
+ LoggerFactory.getLogger(getClass()).error(String.format(
+ "Watcher of [%s] was interrupted but process is still alive. Killing it.", process.getKey()));
+ }
+ alive = false;
+ } finally {
+ process.hardKill();
+ }
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java
new file mode 100644
index 00000000000..93fcb831c83
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java
new file mode 100644
index 00000000000..3c0c7be0208
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+/**
+ * Used to verify that pings were sent or not.
+ */
+public class CallVerifierJmxConnector extends RmiJmxConnector {
+
+ boolean askedPing = false;
+
+ CallVerifierJmxConnector(Timeouts timeouts) {
+ super(timeouts);
+ }
+
+ @Override
+ public void ping(ProcessRef process) {
+ askedPing = true;
+ super.ping(process);
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java
new file mode 100644
index 00000000000..0df9ee9f023
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+public class ImpossibleToConnectJmxConnector implements JmxConnector {
+ @Override
+ public void connect(JavaCommand command, ProcessRef processRef) {
+ throw new IllegalStateException("Test - Impossible to connect to JMX");
+ }
+
+ @Override
+ public void ping(ProcessRef process) {
+
+ }
+
+ @Override
+ public boolean isReady(ProcessRef process) {
+ return false;
+ }
+
+ @Override
+ public void terminate(ProcessRef process) {
+
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/InfiniteTerminationRmiConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/InfiniteTerminationRmiConnector.java
new file mode 100644
index 00000000000..b252379ae8d
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/InfiniteTerminationRmiConnector.java
@@ -0,0 +1,38 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+public class InfiniteTerminationRmiConnector extends RmiJmxConnector {
+
+ InfiniteTerminationRmiConnector(Timeouts timeouts) {
+ super(timeouts);
+ }
+
+ @Override
+ public void terminate(ProcessRef processRef) {
+ try {
+ while (true) {
+ Thread.sleep(50L);
+ }
+ } catch (Exception e) {
+
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java
new file mode 100644
index 00000000000..66654b0f1fc
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java
@@ -0,0 +1,79 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.Properties;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class JavaCommandTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void test_parameters() throws Exception {
+ JavaCommand command = new JavaCommand("es");
+
+ command.setArgument("first_arg", "val1");
+ Properties args = new Properties();
+ args.setProperty("second_arg", "val2");
+ command.setArguments(args);
+
+ command.setJmxPort(1234);
+ command.setClassName("org.sonar.ElasticSearch");
+ command.setEnvVariable("BUILD_ID", "1000");
+ File tempDir = temp.newFolder();
+ command.setTempDir(tempDir);
+ File workDir = temp.newFolder();
+ command.setWorkDir(workDir);
+ command.addClasspath("lib/*.jar");
+ command.addClasspath("conf/*.xml");
+ command.addJavaOption("-Xmx128m");
+
+ assertThat(command.toString()).isNotNull();
+ assertThat(command.getClasspath()).containsOnly("lib/*.jar", "conf/*.xml");
+ assertThat(command.getJavaOptions()).containsOnly("-Xmx128m", "-Djava.io.tmpdir=" + tempDir.getAbsolutePath());
+ assertThat(command.getWorkDir()).isSameAs(workDir);
+ assertThat(command.getJmxPort()).isEqualTo(1234);
+ assertThat(command.getClassName()).isEqualTo("org.sonar.ElasticSearch");
+ assertThat(command.getEnvVariables().get("BUILD_ID")).isEqualTo("1000");
+
+ // copy current env variables
+ assertThat(command.getEnvVariables().size()).isGreaterThan(1);
+ }
+
+ @Test
+ public void test_debug_mode() throws Exception {
+ JavaCommand command = new JavaCommand("es");
+ assertThat(command.isDebugMode()).isFalse();
+
+ command.addJavaOption("-Xmx512m");
+ assertThat(command.isDebugMode()).isFalse();
+
+ command.addJavaOption("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005");
+ assertThat(command.isDebugMode()).isTrue();
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java
new file mode 100644
index 00000000000..80a02cd39c6
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java
@@ -0,0 +1,43 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.junit.Test;
+import org.sonar.process.NetworkUtils;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+
+public class JavaProcessLauncherTest {
+
+ @Test
+ public void fail_to_launch() throws Exception {
+ JavaCommand command = new JavaCommand("test").setJmxPort(NetworkUtils.freePort());
+ JavaProcessLauncher launcher = new JavaProcessLauncher(new Timeouts());
+ try {
+ // command is not correct (missing options), java.lang.ProcessBuilder#start()
+ // throws an exception
+ launcher.launch(command);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("Fail to launch test");
+ }
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java
new file mode 100644
index 00000000000..291b91fbad4
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java
@@ -0,0 +1,442 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import com.github.kevinsawicki.http.HttpRequest;
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.Timeout;
+import org.sonar.process.NetworkUtils;
+import org.sonar.process.State;
+import org.sonar.process.SystemExit;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.ConnectException;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.mockito.Mockito.mock;
+
+public class MonitorTest {
+
+ static File testJar;
+ Monitor monitor;
+ SystemExit exit = mock(SystemExit.class);
+
+ /**
+ * Find the JAR file containing the test apps. Classes can't be moved in sonar-process-monitor because
+ * they require sonar-process dependencies when executed here (sonar-process, commons-*, ...).
+ */
+ @BeforeClass
+ public static void initTestJar() {
+ File targetDir = new File("server/sonar-process/target");
+ if (!targetDir.exists() || !targetDir.isDirectory()) {
+ targetDir = new File("../sonar-process/target");
+ }
+ if (!targetDir.exists() || !targetDir.isDirectory()) {
+ throw new IllegalStateException("target dir of sonar-process module not found. Please build it.");
+ }
+ Collection<File> jars = FileUtils.listFiles(targetDir, new String[] {"jar"}, false);
+ for (File jar : jars) {
+ if (jar.getName().startsWith("sonar-process-") && jar.getName().endsWith("-test-jar-with-dependencies.jar")) {
+ testJar = jar;
+ return;
+ }
+ }
+ throw new IllegalStateException("No sonar-process-*-test-jar-with-dependencies.jar in " + targetDir);
+ }
+
+ /**
+ * Safeguard
+ */
+ @Rule
+ public Timeout globalTimeout = new Timeout(10000);
+
+ /**
+ * Temporary directory is used to interact with monitored processes, which write in it.
+ */
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ /**
+ * Safeguard
+ */
+ @After
+ public void tearDown() throws Exception {
+ try {
+ if (monitor != null) {
+ monitor.stop();
+ }
+ } catch (Throwable ignored) {
+ }
+ }
+
+ @Test
+ public void fail_to_start_if_no_commands() throws Exception {
+ monitor = newDefaultMonitor();
+ try {
+ monitor.start(Collections.<JavaCommand>emptyList());
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessage("At least one command is required");
+ }
+ }
+
+ @Test
+ public void fail_to_start_multiple_times() throws Exception {
+ monitor = newDefaultMonitor();
+ monitor.start(Arrays.asList(newStandardProcessCommand()));
+ boolean failed = false;
+ try {
+ monitor.start(Arrays.asList(newStandardProcessCommand()));
+ } catch (IllegalStateException e) {
+ failed = e.getMessage().equals("Can not start multiple times");
+ }
+ monitor.stop();
+ assertThat(failed);
+ }
+
+ @Test
+ public void start_then_stop_gracefully() throws Exception {
+ monitor = newDefaultMonitor();
+ HttpProcessClient client = new HttpProcessClient("test");
+ // blocks until started
+ monitor.start(Arrays.asList(client.newCommand()));
+
+ assertThat(client.isReady()).isTrue();
+ assertThat(client.wasReadyAt()).isLessThanOrEqualTo(System.currentTimeMillis());
+
+ // blocks until stopped
+ monitor.stop();
+ assertThat(client.isReady()).isFalse();
+ assertThat(client.wasGracefullyTerminated()).isTrue();
+ assertThat(monitor.getState()).isEqualTo(State.STOPPED);
+ }
+
+ @Test
+ public void start_then_stop_sequence_of_commands() throws Exception {
+ monitor = newDefaultMonitor();
+ HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2");
+ monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
+
+ // start p2 when p1 is fully started (ready)
+ assertThat(p1.isReady()).isTrue();
+ assertThat(p2.isReady()).isTrue();
+ assertThat(p2.wasStartingAt()).isGreaterThanOrEqualTo(p1.wasReadyAt());
+
+ monitor.stop();
+
+ // stop in inverse order
+ assertThat(p1.isReady()).isFalse();
+ assertThat(p2.isReady()).isFalse();
+ assertThat(p1.wasGracefullyTerminated()).isTrue();
+ assertThat(p2.wasGracefullyTerminated()).isTrue();
+ assertThat(p2.wasGracefullyTerminatedAt()).isLessThanOrEqualTo(p1.wasGracefullyTerminatedAt());
+ }
+
+ @Test
+ public void fail_to_connect_to_jmx() throws Exception {
+ Timeouts timeouts = new Timeouts();
+ monitor = new Monitor(new JavaProcessLauncher(timeouts),
+ new ImpossibleToConnectJmxConnector(), timeouts, exit);
+
+ HttpProcessClient p1 = new HttpProcessClient("p1");
+ try {
+ monitor.start(Arrays.asList(p1.newCommand()));
+ fail();
+ } catch (Exception e) {
+ // process was correctly launched, but there was a problem with RMI
+ assertThat(p1.isReady()).isFalse();
+ assertThat(p1.wasGracefullyTerminated()).isFalse();
+ }
+ }
+
+ @Test
+ public void terminate_all_processes_if_monitor_shutdowns() throws Exception {
+ monitor = newDefaultMonitor();
+ HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2");
+ monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
+ assertThat(p1.isReady()).isTrue();
+ assertThat(p2.isReady()).isTrue();
+
+ // emulate CTRL-C
+ monitor.getShutdownHook().run();
+ monitor.getShutdownHook().join();
+
+ assertThat(p1.wasGracefullyTerminated()).isTrue();
+ assertThat(p2.wasGracefullyTerminated()).isTrue();
+ }
+
+ @Test
+ public void terminate_all_processes_if_one_monitored_process_shutdowns() throws Exception {
+ monitor = newDefaultMonitor();
+ HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2");
+ monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
+ assertThat(p1.isReady()).isTrue();
+ assertThat(p2.isReady()).isTrue();
+
+ // kill p1 -> waiting for detection by monitor than termination of p2
+ p1.kill();
+ monitor.awaitTermination();
+
+ assertThat(p1.isReady()).isFalse();
+ assertThat(p2.isReady()).isFalse();
+ assertThat(p1.wasGracefullyTerminated()).isFalse();
+ assertThat(p2.wasGracefullyTerminated()).isTrue();
+ }
+
+ @Test
+ public void terminate_all_processes_if_one_fails_to_start() throws Exception {
+ monitor = newDefaultMonitor();
+ HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2", -1, NetworkUtils.freePort());
+ try {
+ monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
+ fail();
+ } catch (Exception expected) {
+ assertThat(p1.wasReady()).isTrue();
+ assertThat(p2.wasReady()).isFalse();
+ assertThat(p1.wasGracefullyTerminated()).isTrue();
+ // self "gracefully terminated", even if startup went bad
+ assertThat(p2.wasGracefullyTerminated()).isTrue();
+ }
+ }
+
+ @Test
+ public void kill_process_if_too_long_to_request_gracefully_termination() throws Exception {
+ Timeouts timeouts = new Timeouts();
+ timeouts.setTerminationTimeout(100L);
+ monitor = new Monitor(new JavaProcessLauncher(timeouts),
+ new InfiniteTerminationRmiConnector(timeouts), timeouts, exit);
+
+ HttpProcessClient p1 = new HttpProcessClient("p1");
+ monitor.start(Arrays.asList(p1.newCommand()));
+ assertThat(p1.isReady()).isTrue();
+
+ monitor.stop();
+ assertThat(p1.isReady()).isFalse();
+ }
+
+ @Test
+ public void kill_process_if_fail_to_request_gracefully_termination() throws Exception {
+ Timeouts timeouts = new Timeouts();
+ timeouts.setTerminationTimeout(100L);
+ monitor = new Monitor(new JavaProcessLauncher(timeouts),
+ new TerminationFailureRmiConnector(timeouts), timeouts, exit);
+
+ HttpProcessClient p1 = new HttpProcessClient("p1");
+ monitor.start(Arrays.asList(p1.newCommand()));
+ assertThat(p1.isReady()).isTrue();
+
+ monitor.stop();
+ assertThat(p1.isReady()).isFalse();
+ }
+
+ @Test
+ public void fail_to_start_if_bad_class_name() throws Exception {
+ monitor = newDefaultMonitor();
+ JavaCommand command = new JavaCommand("test")
+ .addClasspath(testJar.getAbsolutePath())
+ .setClassName("org.sonar.process.test.Unknown")
+ .setJmxPort(NetworkUtils.freePort())
+ .setTempDir(temp.newFolder());
+
+ try {
+ monitor.start(Arrays.asList(command));
+ fail();
+ } catch (Exception e) {
+ // expected
+ // TODO improve, too many stacktraces logged
+ }
+ }
+
+ @Test
+ public void terminate_all_if_one_monitored_process_shutdowns() throws Exception {
+ monitor = newDefaultMonitor();
+ HttpProcessClient client = new HttpProcessClient("test");
+ // blocks until started
+ monitor.start(Arrays.asList(client.newCommand()));
+ assertThat(client.isReady()).isTrue();
+
+ client.kill();
+ assertThat(client.isReady()).isFalse();
+
+ // does not wait, already terminated
+ monitor.awaitTermination();
+
+ // TODO check logs
+ }
+
+ @Test
+ public void fail_if_jmx_port_is_not_available() throws Exception {
+ monitor = newDefaultMonitor();
+ // c1 and c2 have same JMX port
+ int jmxPort = NetworkUtils.freePort();
+ HttpProcessClient p1 = new HttpProcessClient("p1", NetworkUtils.freePort(), jmxPort);
+ HttpProcessClient p2 = new HttpProcessClient("p2", NetworkUtils.freePort(), jmxPort);
+ try {
+ monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
+ fail();
+ } catch (Exception expected) {
+ assertThat(p1.wasReady()).isTrue();
+ assertThat(p2.wasReady()).isFalse();
+ assertThat(p1.isReady()).isFalse();
+ assertThat(p2.isReady()).isFalse();
+ }
+ }
+
+ @Test
+ public void disable_autokill_on_jvm_debug_mode() throws Exception {
+ Timeouts timeouts = new Timeouts();
+ timeouts.setMonitorPingInterval(10L);
+ timeouts.setAutokillPingInterval(10L);
+ timeouts.setAutokillPingTimeout(10L);
+ CallVerifierJmxConnector jmxConnector = new CallVerifierJmxConnector(timeouts);
+ monitor = new Monitor(new JavaProcessLauncher(timeouts), jmxConnector, timeouts, exit);
+
+ JavaCommand command = newStandardProcessCommand()
+ .addJavaOption("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=" + NetworkUtils.freePort());
+ monitor.start(Arrays.asList(command));
+
+ Thread.sleep(20L);
+ assertThat(jmxConnector.askedPing).isFalse();
+
+ monitor.stop();
+ }
+
+ private Monitor newDefaultMonitor() {
+ Timeouts timeouts = new Timeouts();
+ return new Monitor(new JavaProcessLauncher(timeouts), new RmiJmxConnector(timeouts), timeouts, exit);
+ }
+
+ /**
+ * Interaction with {@link org.sonar.process.test.HttpProcess}
+ */
+ private class HttpProcessClient {
+ private final int httpPort;
+ private final String commandKey;
+ private final File tempDir;
+ private int jmxPort;
+
+ private HttpProcessClient(String commandKey) throws IOException {
+ this(commandKey, NetworkUtils.freePort(), NetworkUtils.freePort());
+ }
+
+ /**
+ * Use httpPort=-1 to make server fail to start
+ */
+ private HttpProcessClient(String commandKey, int httpPort, int jmxPort) throws IOException {
+ this.commandKey = commandKey;
+ this.tempDir = temp.newFolder(commandKey);
+ this.httpPort = httpPort;
+ this.jmxPort = jmxPort;
+ }
+
+ JavaCommand newCommand() throws IOException {
+ return new JavaCommand(commandKey)
+ .addClasspath(testJar.getAbsolutePath())
+ .setClassName("org.sonar.process.test.HttpProcess")
+ .setJmxPort(jmxPort)
+ .setArgument("httpPort", String.valueOf(httpPort))
+ .setTempDir(tempDir);
+ }
+
+ /**
+ * @see org.sonar.process.test.HttpProcess
+ */
+ boolean isReady() {
+ try {
+ HttpRequest httpRequest = HttpRequest.get("http://localhost:" + httpPort + "/ping")
+ .readTimeout(500).connectTimeout(500);
+ return httpRequest.ok() && httpRequest.body().equals("ping");
+ } catch (HttpRequest.HttpRequestException e) {
+ if (e.getCause() instanceof ConnectException) {
+ return false;
+ }
+ throw new IllegalStateException("Fail to know the process status", e);
+ }
+ }
+
+ /**
+ * @see org.sonar.process.test.HttpProcess
+ */
+ void kill() {
+ try {
+ HttpRequest.post("http://localhost:" + httpPort + "/kill")
+ .readTimeout(500).connectTimeout(500).ok();
+ } catch (Exception e) {
+ // HTTP request can't be fully processed, as web server hardly
+ // calls "System.exit()"
+ }
+ }
+
+ /**
+ * @see org.sonar.process.test.HttpProcess
+ */
+ boolean wasGracefullyTerminated() {
+ return fileExists("terminatedAt");
+ }
+
+ long wasStartingAt() throws IOException {
+ return readTimeFromFile("startingAt");
+ }
+
+ long wasGracefullyTerminatedAt() throws IOException {
+ return readTimeFromFile("terminatedAt");
+ }
+
+ boolean wasReady() throws IOException {
+ return fileExists("readyAt");
+ }
+
+ long wasReadyAt() throws IOException {
+ return readTimeFromFile("readyAt");
+ }
+
+ private long readTimeFromFile(String filename) throws IOException {
+ File file = new File(tempDir, filename);
+ if (file.isFile() && file.exists()) {
+ return Long.parseLong(FileUtils.readFileToString(file));
+ }
+ throw new IllegalStateException("File does not exist");
+ }
+
+ private boolean fileExists(String filename) {
+ File file = new File(tempDir, filename);
+ return file.isFile() && file.exists();
+ }
+ }
+
+ private JavaCommand newStandardProcessCommand() throws IOException {
+ return new JavaCommand("standard")
+ .addClasspath(testJar.getAbsolutePath())
+ .setClassName("org.sonar.process.test.StandardProcess")
+ .setJmxPort(NetworkUtils.freePort())
+ .setTempDir(temp.newFolder());
+ }
+
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java
new file mode 100644
index 00000000000..9fb9405f21d
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java
@@ -0,0 +1,31 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+public class TerminationFailureRmiConnector extends RmiJmxConnector {
+ TerminationFailureRmiConnector(Timeouts timeouts) {
+ super(timeouts);
+ }
+
+ @Override
+ public void terminate(ProcessRef processRef) {
+ throw new IllegalStateException("Test - fail to send termination request");
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java
new file mode 100644
index 00000000000..5a1d5b590d4
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.junit.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class TimeoutsTest {
+
+ @Test
+ public void test_default_values() throws Exception {
+ Timeouts timeouts = new Timeouts();
+ assertThat(timeouts.getMonitorPingInterval()).isGreaterThan(1000L);
+ assertThat(timeouts.getAutokillPingInterval()).isGreaterThan(1000L);
+ assertThat(timeouts.getAutokillPingTimeout()).isGreaterThan(1000L);
+ assertThat(timeouts.getTerminationTimeout()).isGreaterThan(1000L);
+ assertThat(timeouts.getJmxConnectionTimeout()).isGreaterThan(1000L);
+ }
+
+ @Test
+ public void test_values() throws Exception {
+ Timeouts timeouts = new Timeouts();
+ timeouts.setAutokillPingInterval(1L);
+ timeouts.setAutokillPingTimeout(2L);
+ timeouts.setTerminationTimeout(3L);
+ timeouts.setJmxConnectionTimeout(4L);
+ timeouts.setMonitorPingInterval(5L);
+
+ assertThat(timeouts.getAutokillPingInterval()).isEqualTo(1L);
+ assertThat(timeouts.getAutokillPingTimeout()).isEqualTo(2L);
+ assertThat(timeouts.getTerminationTimeout()).isEqualTo(3L);
+ assertThat(timeouts.getJmxConnectionTimeout()).isEqualTo(4L);
+ assertThat(timeouts.getMonitorPingInterval()).isEqualTo(5L);
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java
new file mode 100644
index 00000000000..86f3a47e6ff
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.monitor;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class WatcherThreadTest {
+
+ @Test(timeout = 10000L)
+ public void kill_process_if_watcher_is_interrupted() throws Exception {
+ ProcessRef ref = mock(ProcessRef.class, Mockito.RETURNS_DEEP_STUBS);
+ when(ref.getProcess().waitFor()).thenAnswer(new Answer<Object>() {
+ @Override
+ public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
+ Thread.sleep(Long.MAX_VALUE);
+ return 0;
+ }
+ });
+ Monitor monitor = mock(Monitor.class);
+
+ WatcherThread watcher = new WatcherThread(ref, monitor);
+ watcher.start();
+ Thread.sleep(50L);
+ watcher.interrupt();
+
+ verify(ref).hardKill();
+ }
+}
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt
new file mode 100644
index 00000000000..65b98c522da
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt
@@ -0,0 +1 @@
+0PZz+G+f8mjr3sPn4+AhHg== \ No newline at end of file
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt
new file mode 100644
index 00000000000..b33e179e5c8
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt
@@ -0,0 +1 @@
+badbadbad== \ No newline at end of file
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt
new file mode 100644
index 00000000000..ab83e4adc03
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt
@@ -0,0 +1,3 @@
+
+ 0PZz+G+f8mjr3sPn4+AhHg==
+
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt
new file mode 100644
index 00000000000..23f5ecf5104
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt
@@ -0,0 +1 @@
+IBxEUxZ41c8XTxyaah1Qlg== \ No newline at end of file
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml b/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml
new file mode 100644
index 00000000000..298193e01fa
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml
@@ -0,0 +1 @@
+<configuration/>
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties b/server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties
new file mode 100644
index 00000000000..1577a214b3b
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties
@@ -0,0 +1,212 @@
+# This file must contain only ISO 8859-1 characters
+# see http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Properties.html#load(java.io.InputStream)
+#
+# To use an environment variable, use the following syntax : ${env:NAME_OF_ENV_VARIABLE}
+# For example:
+# sonar.jdbc.url= ${env:SONAR_JDBC_URL}
+#
+#
+# See also the file conf/wrapper.conf for JVM advanced settings
+
+
+
+#--------------------------------------------------------------------------------------------------
+# DATABASE
+#
+# IMPORTANT: the embedded H2 database is used by default. It is recommended for tests only.
+# Please use a production-ready database. Supported databases are MySQL, Oracle, PostgreSQL
+# and Microsoft SQLServer.
+
+# Permissions to create tables, indices and triggers must be granted to JDBC user.
+# The schema must be created first.
+sonar.jdbc.username=sonar
+sonar.jdbc.password=sonar
+
+#----- Embedded database H2
+# Note: it does not accept connections from remote hosts, so the
+# SonarQube server and the maven plugin must be executed on the same host.
+
+# Comment the following line to deactivate the default embedded database.
+sonar.jdbc.url=jdbc:h2:tcp://localhost:9092/sonar
+
+# directory containing H2 database files. By default it's the /data directory in the SonarQube installation.
+#sonar.embeddedDatabase.dataDir=
+# H2 embedded database server listening port, defaults to 9092
+#sonar.embeddedDatabase.port=9092
+
+
+#----- MySQL 5.x
+# Comment the embedded database and uncomment the following line to use MySQL
+#sonar.jdbc.url=jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
+
+
+#----- Oracle 10g/11g
+# To connect to Oracle database:
+#
+# - It's recommended to use the latest version of the JDBC driver (ojdbc6.jar).
+# Download it in http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html
+# - Copy the driver to the directory extensions/jdbc-driver/oracle/
+# - If you need to set the schema, please refer to http://jira.codehaus.org/browse/SONAR-5000
+# - Comment the embedded database and uncomment the following line:
+#sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE
+
+
+#----- PostgreSQL 8.x/9.x
+# Comment the embedded database and uncomment the following property to use PostgreSQL.
+# If you don't use the schema named "public", please refer to http://jira.codehaus.org/browse/SONAR-5000
+#sonar.jdbc.url=jdbc:postgresql://localhost/sonar
+
+
+#----- Microsoft SQLServer
+# The Jtds open source driver is available in extensions/jdbc-driver/mssql. More details on http://jtds.sourceforge.net
+#sonar.jdbc.url=jdbc:jtds:sqlserver://localhost/sonar;SelectMethod=Cursor
+
+
+#----- Connection pool settings
+sonar.jdbc.maxActive=20
+sonar.jdbc.maxIdle=5
+sonar.jdbc.minIdle=2
+sonar.jdbc.maxWait=5000
+sonar.jdbc.minEvictableIdleTimeMillis=600000
+sonar.jdbc.timeBetweenEvictionRunsMillis=30000
+
+
+
+#--------------------------------------------------------------------------------------------------
+# WEB SERVER
+
+# Binding IP address. For servers with more than one IP address, this property specifies which
+# address will be used for listening on the specified ports.
+# By default, ports will be used on all IP addresses associated with the server.
+#sonar.web.host=0.0.0.0
+
+# Web context. When set, it must start with forward slash (for example /sonarqube).
+# The default value is root context (empty value).
+#sonar.web.context=
+
+# TCP port for incoming HTTP connections. Disabled when value is -1.
+#sonar.web.port=9000
+
+# TCP port for incoming HTTPS connections. Disabled when value is -1 (default).
+#sonar.web.https.port=-1
+
+# HTTPS - the alias used to for the server certificate in the keystore.
+# If not specified the first key read in the keystore is used.
+#sonar.web.https.keyAlias=
+
+# HTTPS - the password used to access the server certificate from the
+# specified keystore file. The default value is "changeit".
+#sonar.web.https.keyPass=changeit
+
+# HTTPS - the pathname of the keystore file where is stored the server certificate.
+# By default, the pathname is the file ".keystore" in the user home.
+# If keystoreType doesn't need a file use empty value.
+#sonar.web.https.keystoreFile=
+
+# HTTPS - the password used to access the specified keystore file. The default
+# value is the value of sonar.web.https.keyPass.
+#sonar.web.https.keystorePass=
+
+# HTTPS - the type of keystore file to be used for the server certificate.
+# The default value is JKS (Java KeyStore).
+#sonar.web.https.keystoreType=JKS
+
+# HTTPS - the name of the keystore provider to be used for the server certificate.
+# If not specified, the list of registered providers is traversed in preference order
+# and the first provider that supports the keystore type is used (see sonar.web.https.keystoreType).
+#sonar.web.https.keystoreProvider=
+
+# HTTPS - the pathname of the truststore file which contains trusted certificate authorities.
+# By default, this would be the cacerts file in your JRE.
+# If truststoreFile doesn't need a file use empty value.
+#sonar.web.https.truststoreFile=
+
+# HTTPS - the password used to access the specified truststore file.
+#sonar.web.https.truststorePass=
+
+# HTTPS - the type of truststore file to be used.
+# The default value is JKS (Java KeyStore).
+#sonar.web.https.truststoreType=JKS
+
+# HTTPS - the name of the truststore provider to be used for the server certificate.
+# If not specified, the list of registered providers is traversed in preference order
+# and the first provider that supports the truststore type is used (see sonar.web.https.truststoreType).
+#sonar.web.https.truststoreProvider=
+
+# HTTPS - whether to enable client certificate authentication.
+# The default is false (client certificates disabled).
+# Other possible values are 'want' (certificates will be requested, but not required),
+# and 'true' (certificates are required).
+#sonar.web.https.clientAuth=false
+
+# The maximum number of connections that the server will accept and process at any given time.
+# When this number has been reached, the server will not accept any more connections until
+# the number of connections falls below this value. The operating system may still accept connections
+# based on the sonar.web.connections.acceptCount property. The default value is 50 for each
+# enabled connector.
+#sonar.web.http.maxThreads=50
+#sonar.web.https.maxThreads=50
+
+# The minimum number of threads always kept running. The default value is 5 for each
+# enabled connector.
+#sonar.web.http.minThreads=5
+#sonar.web.https.minThreads=5
+
+# The maximum queue length for incoming connection requests when all possible request processing
+# threads are in use. Any requests received when the queue is full will be refused.
+# The default value is 25 for each enabled connector.
+#sonar.web.http.acceptCount=25
+#sonar.web.https.acceptCount=25
+
+# Access logs are generated in the file logs/access.log. This file is rolled over when it's 5Mb.
+# An archive of 3 files is kept in the same directory.
+# Access logs are enabled by default.
+#sonar.web.accessLogs.enable=true
+
+# TCP port for incoming AJP connections. Disabled when value is -1.
+# sonar.ajp.port=9009
+
+
+
+#--------------------------------------------------------------------------------------------------
+# UPDATE CENTER
+
+# The Update Center requires an internet connection to request http://update.sonarsource.org
+# It is enabled by default.
+#sonar.updatecenter.activate=true
+
+# HTTP proxy (default none)
+#http.proxyHost=
+#http.proxyPort=
+
+# NT domain name if NTLM proxy is used
+#http.auth.ntlm.domain=
+
+# SOCKS proxy (default none)
+#socksProxyHost=
+#socksProxyPort=
+
+# proxy authentication. The 2 following properties are used for HTTP and SOCKS proxies.
+#http.proxyUser=
+#http.proxyPassword=
+
+
+#--------------------------------------------------------------------------------------------------
+# NOTIFICATIONS
+
+# Delay in seconds between processing of notification queue. Default is 60.
+#sonar.notifications.delay=60
+
+
+#--------------------------------------------------------------------------------------------------
+# PROFILING
+# Level of information displayed in the logs: NONE (default), BASIC (functional information) and FULL (functional and technical details)
+#sonar.log.profilingLevel=NONE
+
+
+#--------------------------------------------------------------------------------------------------
+# DEVELOPMENT MODE
+# Only for debugging
+
+# Set to true to apply Ruby on Rails code changes on the fly
+#sonar.rails.dev=false
diff --git a/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties b/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties
new file mode 100644
index 00000000000..5c06e58a32e
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties
@@ -0,0 +1,3 @@
+hello: world
+foo=bar
+java.io.tmpdir=/should/be/overridden
diff --git a/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar b/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar
new file mode 100644
index 00000000000..6dfd458329a
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar
Binary files differ
diff --git a/server/sonar-process/pom.xml b/server/sonar-process/pom.xml
new file mode 100644
index 00000000000..25413fd7ce9
--- /dev/null
+++ b/server/sonar-process/pom.xml
@@ -0,0 +1,108 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <parent>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>server</artifactId>
+ <version>4.5-SNAPSHOT</version>
+ <relativePath>../</relativePath>
+ </parent>
+ <modelVersion>4.0.0</modelVersion>
+
+ <artifactId>sonar-process</artifactId>
+ <name>SonarQube :: Process</name>
+
+ <dependencies>
+
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>ch.qos.logback</groupId>
+ <artifactId>logback-classic</artifactId>
+ </dependency>
+
+ <dependency>
+ <groupId>commons-codec</groupId>
+ <artifactId>commons-codec</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>commons-lang</groupId>
+ <artifactId>commons-lang</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.easytesting</groupId>
+ <artifactId>fest-assert</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-all</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.eclipse.jetty</groupId>
+ <artifactId>jetty-server</artifactId>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-jar-plugin</artifactId>
+ <executions>
+ <execution>
+ <goals>
+ <goal>test-jar</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-assembly-plugin</artifactId>
+ <configuration>
+ <descriptors>
+ <descriptor>test-jar-with-dependencies.xml</descriptor>
+ </descriptors>
+ </configuration>
+ <executions>
+ <execution>
+ <phase>package</phase>
+ <goals>
+ <goal>single</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java b/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java
new file mode 100644
index 00000000000..204ae1a6b6b
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java
@@ -0,0 +1,133 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+
+import javax.annotation.Nullable;
+import javax.crypto.KeyGenerator;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.File;
+import java.io.IOException;
+import java.security.Key;
+import java.security.SecureRandom;
+
+final class AesCipher implements Cipher {
+
+ // Can't be increased because of Java 6 policy files :
+ // https://confluence.terena.org/display/~visser/No+256+bit+ciphers+for+Java+apps
+ // http://java.sun.com/javase/6/webnotes/install/jre/README
+ public static final int KEY_SIZE_IN_BITS = 128;
+
+ private static final String CRYPTO_KEY = "AES";
+
+ /**
+ * Duplication from CoreProperties.ENCRYPTION_SECRET_KEY_PATH
+ */
+ static final String ENCRYPTION_SECRET_KEY_PATH = "sonar.secretKeyPath";
+
+ private String pathToSecretKey;
+
+ AesCipher(@Nullable String pathToSecretKey) {
+ this.pathToSecretKey = pathToSecretKey;
+ }
+
+ @Override
+ public String encrypt(String clearText) {
+ try {
+ javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY);
+ cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, loadSecretFile());
+ return new String(Base64.encodeBase64(cipher.doFinal(clearText.getBytes("UTF-8"))));
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ @Override
+ public String decrypt(String encryptedText) {
+ try {
+ javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY);
+ cipher.init(javax.crypto.Cipher.DECRYPT_MODE, loadSecretFile());
+ byte[] cipherData = cipher.doFinal(Base64.decodeBase64(StringUtils.trim(encryptedText)));
+ return new String(cipherData);
+ } catch (RuntimeException e) {
+ throw e;
+ } catch (Exception e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ /**
+ * This method checks the existence of the file, but not the validity of the contained key.
+ */
+ boolean hasSecretKey() {
+ String path = getPathToSecretKey();
+ if (StringUtils.isNotBlank(path)) {
+ File file = new File(path);
+ return file.exists() && file.isFile();
+ }
+ return false;
+ }
+
+ private Key loadSecretFile() throws IOException {
+ String path = getPathToSecretKey();
+ return loadSecretFileFromFile(path);
+ }
+
+ Key loadSecretFileFromFile(String path) throws IOException {
+ if (StringUtils.isBlank(path)) {
+ throw new IllegalStateException("Secret key not found. Please set the property " + ENCRYPTION_SECRET_KEY_PATH);
+ }
+ File file = new File(path);
+ if (!file.exists() || !file.isFile()) {
+ throw new IllegalStateException("The property " + ENCRYPTION_SECRET_KEY_PATH + " does not link to a valid file: " + path);
+ }
+ String s = FileUtils.readFileToString(file);
+ if (StringUtils.isBlank(s)) {
+ throw new IllegalStateException("No secret key in the file: " + path);
+ }
+ return new SecretKeySpec(Base64.decodeBase64(StringUtils.trim(s)), CRYPTO_KEY);
+ }
+
+ String generateRandomSecretKey() {
+ try {
+ KeyGenerator keyGen = KeyGenerator.getInstance(CRYPTO_KEY);
+ keyGen.init(KEY_SIZE_IN_BITS, new SecureRandom());
+ SecretKey secretKey = keyGen.generateKey();
+ return new String(Base64.encodeBase64(secretKey.getEncoded()));
+
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to generate secret key", e);
+ }
+ }
+
+ String getPathToSecretKey() {
+ if (StringUtils.isBlank(pathToSecretKey)) {
+ pathToSecretKey = new File(FileUtils.getUserDirectoryPath(), ".sonar/sonar-secret.txt").getPath();
+ }
+ return pathToSecretKey;
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java b/server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java
new file mode 100644
index 00000000000..5eb3eecd541
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java
@@ -0,0 +1,35 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.apache.commons.codec.binary.Base64;
+
+final class Base64Cipher implements Cipher {
+ @Override
+ public String encrypt(String clearText) {
+ return new String(Base64.encodeBase64(clearText.getBytes()));
+ }
+
+ @Override
+ public String decrypt(String encryptedText) {
+ return new String(Base64.decodeBase64(encryptedText));
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/Cipher.java b/server/sonar-process/src/main/java/org/sonar/process/Cipher.java
new file mode 100644
index 00000000000..4c437057757
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/Cipher.java
@@ -0,0 +1,27 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+interface Cipher {
+ String encrypt(String clearText);
+
+ String decrypt(String encryptedText);
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java b/server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java
new file mode 100644
index 00000000000..b4f86457555
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java
@@ -0,0 +1,70 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang.text.StrSubstitutor;
+
+import java.io.File;
+import java.io.FileReader;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Properties;
+
+public final class ConfigurationUtils {
+
+ private ConfigurationUtils() {
+ // Utility class
+ }
+
+ public static Properties interpolateVariables(Properties properties, Map<String, String> variables) {
+ Properties result = new Properties();
+ Enumeration keys = properties.keys();
+ while (keys.hasMoreElements()) {
+ String key = (String) keys.nextElement();
+ String value = (String) properties.get(key);
+ String interpolatedValue = StrSubstitutor.replace(value, variables, "${env:", "}");
+ result.setProperty(key, interpolatedValue);
+ }
+ return result;
+ }
+
+ static Props loadPropsFromCommandLineArgs(String[] args) {
+ if (args.length != 1) {
+ throw new IllegalArgumentException("Only a single command-line argument is accepted " +
+ "(absolute path to configuration file)");
+ }
+
+ File propertyFile = new File(args[0]);
+ Properties properties = new Properties();
+ FileReader reader = null;
+ try {
+ reader = new FileReader(propertyFile);
+ properties.load(reader);
+ } catch (Exception e) {
+ throw new IllegalStateException("Could not read properties from file: " + args[0], e);
+ } finally {
+ IOUtils.closeQuietly(reader);
+ FileUtils.deleteQuietly(propertyFile);
+ }
+ return new Props(properties);
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/Encryption.java b/server/sonar-process/src/main/java/org/sonar/process/Encryption.java
new file mode 100644
index 00000000000..cca05e6c780
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/Encryption.java
@@ -0,0 +1,64 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import javax.annotation.Nullable;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * @since 3.0
+ */
+public final class Encryption {
+
+ private static final String BASE64_ALGORITHM = "b64";
+
+ private static final String AES_ALGORITHM = "aes";
+ private final AesCipher aesCipher;
+
+ private final Map<String, Cipher> ciphers = new HashMap<String, Cipher>();
+ private static final Pattern ENCRYPTED_PATTERN = Pattern.compile("\\{(.*?)\\}(.*)");
+
+ public Encryption(@Nullable String pathToSecretKey) {
+ aesCipher = new AesCipher(pathToSecretKey);
+ ciphers.put(BASE64_ALGORITHM, new Base64Cipher());
+ ciphers.put(AES_ALGORITHM, aesCipher);
+ }
+
+ public boolean isEncrypted(String value) {
+ return value.indexOf('{') == 0 && value.indexOf('}') > 1;
+ }
+
+ public String decrypt(String encryptedText) {
+ Matcher matcher = ENCRYPTED_PATTERN.matcher(encryptedText);
+ if (matcher.matches()) {
+ Cipher cipher = ciphers.get(matcher.group(1).toLowerCase(Locale.ENGLISH));
+ if (cipher != null) {
+ return cipher.decrypt(matcher.group(2));
+ }
+ }
+ return encryptedText;
+ }
+
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java b/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java
new file mode 100644
index 00000000000..c0cf02bcc89
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java
@@ -0,0 +1,81 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import javax.management.MBeanServer;
+import javax.management.MalformedObjectNameException;
+import javax.management.ObjectName;
+import javax.management.remote.JMXServiceURL;
+
+import java.lang.management.ManagementFactory;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.MalformedURLException;
+
+public class JmxUtils {
+
+ private JmxUtils() {
+ // only static stuff
+ }
+
+ public static final String DOMAIN = "org.sonar";
+ public static final String NAME_PROPERTY = "name";
+
+ public static final String WEB_SERVER_NAME = "web";
+ public static final String SEARCH_SERVER_NAME = "search";
+
+ public static ObjectName objectName(String name) {
+ try {
+ return new ObjectName(DOMAIN, NAME_PROPERTY, name);
+ } catch (MalformedObjectNameException e) {
+ throw new IllegalStateException("Cannot create ObjectName for " + name, e);
+ }
+ }
+
+ public static void registerMBean(Object mbean, String name) {
+ try {
+ MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
+ ObjectName oName = objectName(name);
+ // Check if already registered in JVM (might run multiple instance in JUnits)
+ if (mbeanServer.isRegistered(oName)) {
+ mbeanServer.unregisterMBean(oName);
+ }
+ mbeanServer.registerMBean(mbean, oName);
+ } catch (RuntimeException re) {
+ throw re;
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to register JMX MBean named " + name, e);
+ }
+ }
+
+ public static JMXServiceURL serviceUrl(InetAddress host, int port) {
+ String address = host.getHostAddress();
+ if (host instanceof Inet6Address) {
+ // See http://docs.oracle.com/javase/7/docs/api/javax/management/remote/JMXServiceURL.html
+ // "The host is a host name, an IPv4 numeric host address, or an IPv6 numeric address enclosed in square brackets."
+ address = String.format("[%s]", address);
+ }
+ try {
+ return new JMXServiceURL("rmi", address, port, String.format("/jndi/rmi://%s:%d/jmxrmi", address, port));
+ } catch (MalformedURLException e) {
+ throw new IllegalStateException("JMX url does not look well formed", e);
+ }
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java b/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java
new file mode 100644
index 00000000000..a492ac751ca
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+public class Lifecycle {
+
+ private State state = State.INIT;
+
+ public State getState() {
+ return state;
+ }
+
+ public synchronized boolean tryToMoveTo(State to) {
+ if (state.ordinal() < to.ordinal()) {
+ state = to;
+ return true;
+ }
+ return false;
+ }
+
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ Lifecycle lifecycle = (Lifecycle) o;
+ return state == lifecycle.state;
+ }
+
+ @Override
+ public int hashCode() {
+ return state.hashCode();
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java b/server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java
new file mode 100644
index 00000000000..2302a626d2c
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java
@@ -0,0 +1,71 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+
+public class LoopbackAddress {
+
+ private static InetAddress instance;
+
+ private LoopbackAddress() {
+ // only static stuff
+ }
+
+ /**
+ * Quite similar to {@code InetAddress.getLoopbackAddress()} which was introduced in Java 7. This
+ * method aims to support Java 6. It returns an IPv4 address, but not IPv6 in order to
+ * support {@code -Djava.net.preferIPv4Stack=true} which is recommended for Elasticsearch.
+ */
+ public static InetAddress get() {
+ if (instance == null) {
+ try {
+ instance = doGet(NetworkInterface.getNetworkInterfaces());
+ } catch (SocketException e) {
+ throw new IllegalStateException("Fail to browse network interfaces", e);
+ }
+
+ }
+ return instance;
+ }
+
+ static InetAddress doGet(Enumeration<NetworkInterface> ifaces) {
+ InetAddress result = null;
+ while (ifaces.hasMoreElements() && result == null) {
+ NetworkInterface iface = ifaces.nextElement();
+ Enumeration<InetAddress> addresses = iface.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ InetAddress addr = addresses.nextElement();
+ if (addr.isLoopbackAddress() && addr instanceof Inet4Address) {
+ result = addr;
+ break;
+ }
+ }
+ }
+ if (result == null) {
+ throw new IllegalStateException("Impossible to get a IPv4 loopback address");
+ }
+ return result;
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/MessageException.java b/server/sonar-process/src/main/java/org/sonar/process/MessageException.java
new file mode 100644
index 00000000000..5b86ef66c64
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/MessageException.java
@@ -0,0 +1,36 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+public class MessageException extends RuntimeException {
+ public MessageException(String message) {
+ super(message);
+ }
+
+ /**
+ * Does not fill in the stack trace
+ *
+ * @see Throwable#fillInStackTrace()
+ */
+ @Override
+ public synchronized Throwable fillInStackTrace() {
+ return this;
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java b/server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java
new file mode 100644
index 00000000000..2389fa5aa18
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java
@@ -0,0 +1,86 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.lang.StringUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class MinimumViableSystem {
+
+ private final Map<String, String> requiredJavaOptions = new HashMap<String, String>();
+
+ public MinimumViableSystem setRequiredJavaOption(String propertyKey, String expectedValue) {
+ requiredJavaOptions.put(propertyKey, expectedValue);
+ return this;
+ }
+
+ /**
+ * Entry point for all checks
+ */
+ public void check() {
+ checkJavaVersion();
+ checkJavaOptions();
+ checkWritableTempDir();
+ }
+
+ /**
+ * Verify that temp directory is writable
+ */
+ private void checkWritableTempDir() {
+ checkWritableDir(System.getProperty("java.io.tmpdir"));
+ }
+
+ void checkWritableDir(String tempPath) {
+ try {
+ File tempFile = File.createTempFile("check", "tmp", new File(tempPath));
+ FileUtils.deleteQuietly(tempFile);
+ } catch (IOException e) {
+ throw new IllegalStateException(String.format("Temp directory is not writable: %s", tempPath), e);
+ }
+ }
+
+ void checkJavaOptions() {
+ for (Map.Entry<String, String> entry : requiredJavaOptions.entrySet()) {
+ String value = System.getProperty(entry.getKey());
+ if (!StringUtils.equals(value, entry.getValue())) {
+ throw new MessageException(String.format(
+ "JVM option '%s' must be set to '%s'. Got '%s'", entry.getKey(), entry.getValue(), StringUtils.defaultString(value)));
+ }
+ }
+ }
+
+ void checkJavaVersion() {
+ String javaVersion = System.getProperty("java.specification.version");
+ checkJavaVersion(javaVersion);
+ }
+
+ void checkJavaVersion(String javaVersion) {
+ if (!javaVersion.startsWith("1.6") && !javaVersion.startsWith("1.7") && !javaVersion.startsWith("1.8")) {
+ // still better than "java.lang.UnsupportedClassVersionError: Unsupported major.minor version 49.0
+ throw new MessageException(String.format("Supported versions of Java are 1.6, 1.7 and 1.8. Got %s.", javaVersion));
+ }
+ }
+
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java b/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java
new file mode 100644
index 00000000000..6ee84d00744
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java
@@ -0,0 +1,31 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+public interface MonitoredProcess extends Terminable {
+
+ /**
+ * Starts and blocks until ready
+ */
+ void start();
+
+ void awaitTermination();
+
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java b/server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java
new file mode 100644
index 00000000000..074cb8cf5c2
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java
@@ -0,0 +1,40 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import java.net.ServerSocket;
+
+public class NetworkUtils {
+
+ private NetworkUtils() {
+ // only static stuff
+ }
+
+ public static int freePort() {
+ try {
+ ServerSocket s = new ServerSocket(0);
+ int port = s.getLocalPort();
+ s.close();
+ return port;
+ } catch (Exception e) {
+ throw new IllegalStateException("Can not find an open network port", e);
+ }
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java
new file mode 100644
index 00000000000..79ec79f74b7
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java
@@ -0,0 +1,148 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.slf4j.LoggerFactory;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class ProcessEntryPoint implements ProcessMXBean {
+
+ public static final String PROPERTY_PROCESS_KEY = "process.key";
+ public static final String PROPERTY_AUTOKILL_DISABLED = "process.autokill.disabled";
+ public static final String PROPERTY_AUTOKILL_PING_TIMEOUT = "process.autokill.pingTimeout";
+ public static final String PROPERTY_AUTOKILL_PING_INTERVAL = "process.autokill.pingInterval";
+ public static final String PROPERTY_TERMINATION_TIMEOUT = "process.terminationTimeout";
+
+ private final Props props;
+ private final Lifecycle lifecycle = new Lifecycle();
+ private volatile MonitoredProcess monitoredProcess;
+ private volatile long lastPing = 0L;
+ private volatile StopperThread stopperThread;
+ private final SystemExit exit;
+ private Thread shutdownHook = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ exit.setInShutdownHook();
+ terminate();
+ }
+ });
+
+ ProcessEntryPoint(Props props, SystemExit exit) {
+ this.props = props;
+ this.exit = exit;
+ }
+
+ public Props getProps() {
+ return props;
+ }
+
+ /**
+ * Launch process and waits until it's down
+ */
+ public void launch(MonitoredProcess mp) {
+ if (!lifecycle.tryToMoveTo(State.STARTING)) {
+ throw new IllegalStateException("Already started");
+ }
+ monitoredProcess = mp;
+
+ // TODO check if these properties are available in System Info
+ JmxUtils.registerMBean(this, props.nonNullValue(PROPERTY_PROCESS_KEY));
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
+ if (!props.valueAsBoolean(PROPERTY_AUTOKILL_DISABLED, false)) {
+ // mainly for Java Debugger
+ scheduleAutokill();
+ }
+
+ try {
+ monitoredProcess.start();
+ if (lifecycle.tryToMoveTo(State.STARTED)) {
+ monitoredProcess.awaitTermination();
+ }
+ } catch (Exception ignored) {
+ } finally {
+ terminate();
+ }
+ }
+
+ @Override
+ public boolean isReady() {
+ return lifecycle.getState() == State.STARTED;
+ }
+
+ @Override
+ public void ping() {
+ lastPing = System.currentTimeMillis();
+ }
+
+ /**
+ * Blocks until stopped in a timely fashion (see {@link org.sonar.process.StopperThread})
+ */
+ @Override
+ public void terminate() {
+ if (lifecycle.tryToMoveTo(State.STOPPING)) {
+ stopperThread = new StopperThread(monitoredProcess, Long.parseLong(props.nonNullValue(PROPERTY_TERMINATION_TIMEOUT)));
+ stopperThread.start();
+ }
+ try {
+ // stopperThread is not null for sure
+ // join() does nothing if thread already finished
+ stopperThread.join();
+ lifecycle.tryToMoveTo(State.STOPPED);
+ } catch (InterruptedException e) {
+ // nothing to do, the process is going to be exited
+ }
+ exit.exit(0);
+ }
+
+ private void scheduleAutokill() {
+ final long autokillPingTimeoutMs = props.valueAsInt(PROPERTY_AUTOKILL_PING_TIMEOUT);
+ long autokillPingIntervalMs = props.valueAsInt(PROPERTY_AUTOKILL_PING_INTERVAL);
+ Runnable autokiller = new Runnable() {
+ @Override
+ public void run() {
+ long time = System.currentTimeMillis();
+ if (time - lastPing > autokillPingTimeoutMs) {
+ LoggerFactory.getLogger(getClass()).info(String.format(
+ "Did not receive any ping during %d seconds. Shutting down.", autokillPingTimeoutMs / 1000));
+ terminate();
+ }
+ }
+ };
+ lastPing = System.currentTimeMillis();
+ ScheduledExecutorService monitor = Executors.newScheduledThreadPool(1);
+ monitor.scheduleWithFixedDelay(autokiller, autokillPingIntervalMs, autokillPingIntervalMs, TimeUnit.MILLISECONDS);
+ }
+
+ State getState() {
+ return lifecycle.getState();
+ }
+
+ Thread getShutdownHook() {
+ return shutdownHook;
+ }
+
+ public static ProcessEntryPoint createForArguments(String[] args) {
+ Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args);
+ return new ProcessEntryPoint(props, new SystemExit());
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java
new file mode 100644
index 00000000000..dacddd91847
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java
@@ -0,0 +1,53 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.joran.JoranConfigurator;
+import ch.qos.logback.core.joran.spi.JoranException;
+import ch.qos.logback.core.util.StatusPrinter;
+import org.slf4j.LoggerFactory;
+
+public class ProcessLogging {
+
+ private static final String PATH_LOGS_PROPERTY = "sonar.path.logs";
+
+ public void configure(Props props, String logbackXmlResource) {
+ LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
+ try {
+ JoranConfigurator configurator = new JoranConfigurator();
+ configurator.setContext(context);
+ context.reset();
+ context.putProperty(PATH_LOGS_PROPERTY, props.nonNullValue(PATH_LOGS_PROPERTY));
+ doConfigure(configurator, logbackXmlResource);
+ } catch (JoranException ignored) {
+ // StatusPrinter will handle this
+ }
+ StatusPrinter.printInCaseOfErrorsOrWarnings(context);
+
+ }
+
+ /**
+ * Extracted only for unit testing
+ */
+ void doConfigure(JoranConfigurator configurator, String logbackXmlResource) throws JoranException {
+ configurator.doConfigure(getClass().getResource(logbackXmlResource));
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java
new file mode 100644
index 00000000000..3d024d420eb
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java
@@ -0,0 +1,28 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+public interface ProcessMXBean extends Terminable {
+
+ boolean isReady();
+
+ void ping();
+
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java
new file mode 100644
index 00000000000..bdefa116949
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java
@@ -0,0 +1,77 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.apache.commons.io.IOUtils;
+import org.slf4j.LoggerFactory;
+
+import javax.annotation.Nullable;
+
+public class ProcessUtils {
+
+ private ProcessUtils() {
+ // only static stuff
+ }
+
+ /**
+ * Do not abuse to this method. It uses exceptions to get status.
+ * @return false if process is null or terminated, else true.
+ */
+ public static boolean isAlive(@Nullable Process process) {
+ boolean alive = false;
+ if (process != null) {
+ try {
+ process.exitValue();
+ } catch (IllegalThreadStateException ignored) {
+ alive = true;
+ }
+ }
+ return alive;
+ }
+
+ /**
+ * Destroys process (equivalent to kill -9) if alive
+ * @return true if the process was destroyed, false if process is null or already destroyed.
+ */
+ public static boolean destroyQuietly(@Nullable Process process) {
+ boolean destroyed = false;
+ if (isAlive(process)) {
+ try {
+ process.destroy();
+ while (isAlive(process)) {
+ // destroy() sends the signal, it does not wait for the process to be down
+ Thread.sleep(100L);
+ }
+ destroyed = true;
+ } catch (Exception e) {
+ LoggerFactory.getLogger(ProcessUtils.class).error("Fail to destroy " + process);
+ }
+ }
+ return destroyed;
+ }
+
+ public static void closeStreams(@Nullable Process process) {
+ if (process != null) {
+ IOUtils.closeQuietly(process.getInputStream());
+ IOUtils.closeQuietly(process.getOutputStream());
+ IOUtils.closeQuietly(process.getErrorStream());
+ }
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/Props.java b/server/sonar-process/src/main/java/org/sonar/process/Props.java
new file mode 100644
index 00000000000..b868702eafc
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/Props.java
@@ -0,0 +1,120 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.apache.commons.lang.StringUtils;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+import java.io.File;
+import java.util.Properties;
+
+public class Props {
+
+ private final Properties properties;
+ private final Encryption encryption;
+
+ public Props(Properties props) {
+ this.properties = props;
+ this.encryption = new Encryption(props.getProperty(AesCipher.ENCRYPTION_SECRET_KEY_PATH));
+ }
+
+ public boolean contains(String key) {
+ return properties.containsKey(key);
+ }
+
+ @CheckForNull
+ public String value(String key) {
+ String value = properties.getProperty(key);
+ if (value != null && encryption.isEncrypted(value)) {
+ value = encryption.decrypt(value);
+ }
+ return value;
+ }
+
+ public String nonNullValue(String key) {
+ String value = value(key);
+ if (value == null) {
+ throw new IllegalArgumentException("Missing property: " + key);
+ }
+ return value;
+ }
+
+ @CheckForNull
+ public String value(String key, @Nullable String defaultValue) {
+ String s = value(key);
+ return s == null ? defaultValue : s;
+ }
+
+ public boolean valueAsBoolean(String key) {
+ String s = value(key);
+ return s != null && Boolean.parseBoolean(s);
+ }
+
+ public boolean valueAsBoolean(String key, boolean defaultValue) {
+ String s = value(key);
+ return s != null ? Boolean.parseBoolean(s) : defaultValue;
+ }
+
+ public File nonNullValueAsFile(String key) {
+ String s = value(key);
+ if (s == null) {
+ throw new IllegalArgumentException("Property " + key + " is missing");
+ }
+ return new File(s);
+ }
+
+ @CheckForNull
+ public Integer valueAsInt(String key) {
+ String s = value(key);
+ if (s != null && !"".equals(s)) {
+ try {
+ return Integer.parseInt(s);
+ } catch (NumberFormatException e) {
+ throw new IllegalStateException("Value of property " + key + " is not an integer: " + s, e);
+ }
+ }
+ return null;
+ }
+
+ public int valueAsInt(String key, int defaultValue) {
+ Integer i = valueAsInt(key);
+ return i == null ? defaultValue : i;
+ }
+
+ public Properties rawProperties() {
+ return properties;
+ }
+
+ public Props set(String key, @Nullable String value) {
+ if (value != null) {
+ properties.setProperty(key, value);
+ }
+ return this;
+ }
+
+ public void setDefault(String key, String value) {
+ String s = properties.getProperty(key);
+ if (StringUtils.isBlank(s)) {
+ properties.setProperty(key, value);
+ }
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/State.java b/server/sonar-process/src/main/java/org/sonar/process/State.java
new file mode 100644
index 00000000000..9d773d2bc23
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/State.java
@@ -0,0 +1,26 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+public enum State {
+
+ INIT, STARTING, STARTED, STOPPING, STOPPED
+
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/StopperThread.java b/server/sonar-process/src/main/java/org/sonar/process/StopperThread.java
new file mode 100644
index 00000000000..2d0c6734b30
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/StopperThread.java
@@ -0,0 +1,57 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Gracefully stops process, but exits JVM if too long
+ */
+class StopperThread extends Thread {
+
+ private final Terminable terminable;
+ private final long terminationTimeout;
+
+ StopperThread(Terminable terminable, long terminationTimeout) {
+ super("Stopper");
+ this.terminable = terminable;
+ this.terminationTimeout = terminationTimeout;
+ }
+
+ @Override
+ public void run() {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ Future future = executor.submit(new Runnable() {
+ @Override
+ public void run() {
+ terminable.terminate();
+ }
+ });
+ try {
+ future.get(terminationTimeout, TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ future.cancel(true);
+ executor.shutdownNow();
+ }
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/SystemExit.java b/server/sonar-process/src/main/java/org/sonar/process/SystemExit.java
new file mode 100644
index 00000000000..8f12a210633
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/SystemExit.java
@@ -0,0 +1,52 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Calls {@link System#exit(int)} except from shutdown hooks, to prevent
+ * deadlocks. See http://stackoverflow.com/a/19552359/229031
+ */
+public class SystemExit {
+
+ private final AtomicBoolean inShutdownHook = new AtomicBoolean(false);
+
+ public void exit(int code) {
+ if (!inShutdownHook.get()) {
+ doExit(code);
+ }
+ }
+
+ public boolean isInShutdownHook() {
+ return inShutdownHook.get();
+ }
+
+ /**
+ * Declarative approach. I don't know how to get this lifecycle state from Java API.
+ */
+ public void setInShutdownHook() {
+ inShutdownHook.set(true);
+ }
+
+ void doExit(int code) {
+ System.exit(code);
+ }
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/Terminable.java b/server/sonar-process/src/main/java/org/sonar/process/Terminable.java
new file mode 100644
index 00000000000..a8670609fe2
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/Terminable.java
@@ -0,0 +1,28 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+/**
+ * This term "terminate" is used in order to not conflict with {@link Thread#stop()}.
+ */
+public interface Terminable {
+
+ void terminate();
+}
diff --git a/server/sonar-process/src/main/java/org/sonar/process/package-info.java b/server/sonar-process/src/main/java/org/sonar/process/package-info.java
new file mode 100644
index 00000000000..09da5ce266d
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/package-info.java
@@ -0,0 +1,23 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java b/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java
new file mode 100644
index 00000000000..8350eafaa3e
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java
@@ -0,0 +1,185 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import com.google.common.io.Resources;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.StringUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import javax.crypto.BadPaddingException;
+import java.io.File;
+import java.security.InvalidKeyException;
+import java.security.Key;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+
+
+public class AesCipherTest {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ @Test
+ public void generateRandomSecretKey() {
+ AesCipher cipher = new AesCipher(null);
+
+ String key = cipher.generateRandomSecretKey();
+
+ assertThat(StringUtils.isNotBlank(key)).isTrue();
+ assertThat(Base64.isArrayByteBase64(key.getBytes())).isTrue();
+ }
+
+ @Test
+ public void encrypt() throws Exception {
+ AesCipher cipher = new AesCipher(pathToSecretKey());
+
+ String encryptedText = cipher.encrypt("this is a secret");
+
+ assertThat(StringUtils.isNotBlank(encryptedText)).isTrue();
+ assertThat(Base64.isArrayByteBase64(encryptedText.getBytes())).isTrue();
+ }
+
+ @Test
+ public void encrypt_bad_key() throws Exception {
+ thrown.expect(RuntimeException.class);
+ thrown.expectMessage("Invalid AES key");
+
+ AesCipher cipher = new AesCipher(getPath("bad_secret_key.txt"));
+
+ cipher.encrypt("this is a secret");
+ }
+
+ @Test
+ public void decrypt() throws Exception {
+ AesCipher cipher = new AesCipher(pathToSecretKey());
+
+ // the following value has been encrypted with the key /org/sonar/api/config/AesCipherTest/aes_secret_key.txt
+ String clearText = cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
+
+ assertThat(clearText).isEqualTo("this is a secret");
+ }
+
+ @Test
+ public void decrypt_bad_key() throws Exception {
+ AesCipher cipher = new AesCipher(getPath("bad_secret_key.txt"));
+
+ try {
+ cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
+ fail();
+
+ } catch (RuntimeException e) {
+ assertThat(e.getCause()).isInstanceOf(InvalidKeyException.class);
+ }
+ }
+
+ @Test
+ public void decrypt_other_key() throws Exception {
+ AesCipher cipher = new AesCipher(getPath("other_secret_key.txt"));
+
+ try {
+ // text encrypted with another key
+ cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
+ fail();
+
+ } catch (RuntimeException e) {
+ assertThat(e.getCause()).isInstanceOf(BadPaddingException.class);
+ }
+ }
+
+ @Test
+ public void encryptThenDecrypt() throws Exception {
+ AesCipher cipher = new AesCipher(pathToSecretKey());
+
+ assertThat(cipher.decrypt(cipher.encrypt("foo"))).isEqualTo("foo");
+ }
+
+ @Test
+ public void testDefaultPathToSecretKey() {
+ AesCipher cipher = new AesCipher(null);
+
+ String path = cipher.getPathToSecretKey();
+
+ assertThat(StringUtils.isNotBlank(path)).isTrue();
+ assertThat(new File(path).getName()).isEqualTo("sonar-secret.txt");
+ }
+
+ @Test
+ public void loadSecretKeyFromFile() throws Exception {
+ AesCipher cipher = new AesCipher(null);
+ Key secretKey = cipher.loadSecretFileFromFile(pathToSecretKey());
+ assertThat(secretKey.getAlgorithm()).isEqualTo("AES");
+ assertThat(secretKey.getEncoded().length).isGreaterThan(10);
+ }
+
+ @Test
+ public void loadSecretKeyFromFile_trim_content() throws Exception {
+ String path = getPath("non_trimmed_secret_key.txt");
+ AesCipher cipher = new AesCipher(null);
+
+ Key secretKey = cipher.loadSecretFileFromFile(path);
+
+ assertThat(secretKey.getAlgorithm()).isEqualTo("AES");
+ assertThat(secretKey.getEncoded().length).isGreaterThan(10);
+ }
+
+ @Test
+ public void loadSecretKeyFromFile_file_does_not_exist() throws Exception {
+ thrown.expect(IllegalStateException.class);
+
+ AesCipher cipher = new AesCipher(null);
+ cipher.loadSecretFileFromFile("/file/does/not/exist");
+ }
+
+ @Test
+ public void loadSecretKeyFromFile_no_property() throws Exception {
+ thrown.expect(IllegalStateException.class);
+
+ AesCipher cipher = new AesCipher(null);
+ cipher.loadSecretFileFromFile(null);
+ }
+
+ @Test
+ public void hasSecretKey() throws Exception {
+ AesCipher cipher = new AesCipher(pathToSecretKey());
+
+ assertThat(cipher.hasSecretKey()).isTrue();
+ }
+
+ @Test
+ public void doesNotHaveSecretKey() throws Exception {
+ AesCipher cipher = new AesCipher("/my/twitter/id/is/SimonBrandhof");
+
+ assertThat(cipher.hasSecretKey()).isFalse();
+ }
+
+ private static String getPath(String file) {
+ return Resources.getResource(AesCipherTest.class, "AesCipherTest/" + file).getPath();
+ }
+
+ private static String pathToSecretKey() throws Exception {
+ return getPath("aes_secret_key.txt");
+ }
+
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java b/server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java
new file mode 100644
index 00000000000..2045cd4516d
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.ServerSocket;
+
+public abstract class BaseProcessTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ public static final String DUMMY_OK_APP = "org.sonar.application.DummyOkProcess";
+
+ int freePort;
+ File dummyAppJar;
+ Process proc;
+
+ @Before
+ public void setup() throws IOException {
+ ServerSocket socket = new ServerSocket(0);
+ freePort = socket.getLocalPort();
+ socket.close();
+
+ dummyAppJar = FileUtils.toFile(getClass().getResource("/sonar-dummy-app.jar"));
+ }
+
+ @After
+ public void tearDown() {
+ if (proc != null) {
+ ProcessUtils.destroyQuietly(proc);
+ }
+ }
+
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java
new file mode 100644
index 00000000000..de928b93850
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java
@@ -0,0 +1,95 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import com.google.common.collect.Maps;
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.util.Map;
+import java.util.Properties;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+
+public class ConfigurationUtilsTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void shouldInterpolateVariables() {
+ Properties input = new Properties();
+ input.setProperty("hello", "world");
+ input.setProperty("url", "${env:SONAR_JDBC_URL}");
+ input.setProperty("do_not_change", "${SONAR_JDBC_URL}");
+ Map<String, String> variables = Maps.newHashMap();
+ variables.put("SONAR_JDBC_URL", "jdbc:h2:mem");
+
+ Properties output = ConfigurationUtils.interpolateVariables(input, variables);
+
+ assertThat(output).hasSize(3);
+ assertThat(output.getProperty("hello")).isEqualTo("world");
+ assertThat(output.getProperty("url")).isEqualTo("jdbc:h2:mem");
+ assertThat(output.getProperty("do_not_change")).isEqualTo("${SONAR_JDBC_URL}");
+
+ // input is not changed
+ assertThat(input).hasSize(3);
+ assertThat(input.getProperty("hello")).isEqualTo("world");
+ assertThat(input.getProperty("url")).isEqualTo("${env:SONAR_JDBC_URL}");
+ assertThat(input.getProperty("do_not_change")).isEqualTo("${SONAR_JDBC_URL}");
+ }
+
+ @Test
+ public void loadPropsFromCommandLineArgs_missing_argument() throws Exception {
+ try {
+ ConfigurationUtils.loadPropsFromCommandLineArgs(new String[0]);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e.getMessage()).startsWith("Only a single command-line argument is accepted");
+ }
+ }
+
+ @Test
+ public void loadPropsFromCommandLineArgs_load_properties_from_file() throws Exception {
+ File propsFile = temp.newFile();
+ FileUtils.write(propsFile, "foo=bar");
+
+ Props result = ConfigurationUtils.loadPropsFromCommandLineArgs(new String[] {propsFile.getAbsolutePath()});
+ assertThat(result.value("foo")).isEqualTo("bar");
+ assertThat(result.rawProperties()).hasSize(1);
+ }
+
+ @Test
+ public void loadPropsFromCommandLineArgs_file_does_not_exist() throws Exception {
+ File propsFile = temp.newFile();
+ FileUtils.deleteQuietly(propsFile);
+
+ try {
+ ConfigurationUtils.loadPropsFromCommandLineArgs(new String[]{propsFile.getAbsolutePath()});
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("Could not read properties from file: " + propsFile.getAbsolutePath());
+ }
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java b/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java
new file mode 100644
index 00000000000..0c11856b0fa
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java
@@ -0,0 +1,59 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.junit.Test;
+
+import static org.hamcrest.Matchers.is;
+import static org.junit.Assert.assertThat;
+
+public class EncryptionTest {
+
+ @Test
+ public void isEncrypted() {
+ Encryption encryption = new Encryption(null);
+ assertThat(encryption.isEncrypted("{aes}ADASDASAD"), is(true));
+ assertThat(encryption.isEncrypted("{b64}ADASDASAD"), is(true));
+ assertThat(encryption.isEncrypted("{abc}ADASDASAD"), is(true));
+
+ assertThat(encryption.isEncrypted("{}"), is(false));
+ assertThat(encryption.isEncrypted("{foo"), is(false));
+ assertThat(encryption.isEncrypted("foo{aes}"), is(false));
+ }
+
+ @Test
+ public void decrypt() {
+ Encryption encryption = new Encryption(null);
+ assertThat(encryption.decrypt("{b64}Zm9v"), is("foo"));
+ }
+
+ @Test
+ public void decrypt_unknown_algorithm() {
+ Encryption encryption = new Encryption(null);
+ assertThat(encryption.decrypt("{xxx}Zm9v"), is("{xxx}Zm9v"));
+ }
+
+ @Test
+ public void decrypt_uncrypted_text() {
+ Encryption encryption = new Encryption(null);
+ assertThat(encryption.decrypt("foo"), is("foo"));
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java
new file mode 100644
index 00000000000..599ea5d7a30
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java
@@ -0,0 +1,120 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.junit.Test;
+
+import javax.management.MBeanServer;
+import javax.management.ObjectName;
+import javax.management.remote.JMXServiceURL;
+
+import java.lang.management.ManagementFactory;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.util.Enumeration;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+
+public class JmxUtilsTest {
+
+ class MyBean implements ProcessMXBean {
+
+
+ @Override
+ public void terminate() {
+
+ }
+
+ @Override
+ public void ping() {
+
+ }
+
+ @Override
+ public boolean isReady() {
+ return true;
+ }
+ }
+
+ @Test
+ public void construct_jmx_objectName() throws Exception {
+ MyBean mxBean = new MyBean();
+ ObjectName objectName = JmxUtils.objectName(mxBean.getClass().getSimpleName());
+ assertThat(objectName).isNotNull();
+ assertThat(objectName.getDomain()).isEqualTo(JmxUtils.DOMAIN);
+ assertThat(objectName.getKeyProperty(JmxUtils.NAME_PROPERTY)).isEqualTo(mxBean.getClass().getSimpleName());
+ }
+
+ @Test
+ public void fail_jmx_objectName() throws Exception {
+ try {
+ JmxUtils.objectName(":");
+ fail();
+ } catch (Exception e) {
+ assertThat(e.getMessage()).isEqualTo("Cannot create ObjectName for :");
+ }
+ }
+
+ @Test
+ public void testRegisterMBean() throws Exception {
+ // 0 Get mbServer and create out test MXBean
+ MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
+ MyBean mxBean = new MyBean();
+ ObjectName objectName = JmxUtils.objectName(mxBean.getClass().getSimpleName());
+
+ // 1 assert that mxBean gets registered
+ assertThat(mbeanServer.isRegistered(objectName)).isFalse();
+ JmxUtils.registerMBean(mxBean, mxBean.getClass().getSimpleName());
+ assertThat(mbeanServer.isRegistered(objectName)).isTrue();
+ }
+
+ @Test
+ public void serviceUrl_ipv4() throws Exception {
+ JMXServiceURL url = JmxUtils.serviceUrl(ip(Inet4Address.class), 1234);
+ assertThat(url).isNotNull();
+ assertThat(url.getPort()).isEqualTo(1234);
+ }
+
+ @Test
+ public void serviceUrl_ipv6() throws Exception {
+ JMXServiceURL url = JmxUtils.serviceUrl(ip(Inet6Address.class), 1234);
+ assertThat(url).isNotNull();
+ assertThat(url.getPort()).isEqualTo(1234);
+ }
+
+ private static InetAddress ip(Class inetAddressClass) throws SocketException {
+ Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces();
+ while (ifaces.hasMoreElements()) {
+ NetworkInterface iface = ifaces.nextElement();
+ Enumeration<InetAddress> addresses = iface.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ InetAddress addr = addresses.nextElement();
+ if (addr.getClass().isAssignableFrom(inetAddressClass)) {
+ return addr;
+ }
+ }
+ }
+ throw new IllegalStateException("no ipv4 address");
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java b/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java
new file mode 100644
index 00000000000..23886125212
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java
@@ -0,0 +1,42 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.junit.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class LifecycleTest {
+
+ @Test
+ public void equals_and_hashcode() throws Exception {
+ Lifecycle init = new Lifecycle();
+ assertThat(init.equals(init)).isTrue();
+ assertThat(init.equals(new Lifecycle())).isTrue();
+ assertThat(init.equals("INIT")).isFalse();
+ assertThat(init.equals(null)).isFalse();
+ assertThat(init.hashCode()).isEqualTo(new Lifecycle().hashCode());
+
+ // different state
+ Lifecycle stopping = new Lifecycle();
+ stopping.tryToMoveTo(State.STOPPING);
+ assertThat(stopping).isNotEqualTo(init);
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java b/server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java
new file mode 100644
index 00000000000..6a8819c3a81
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java
@@ -0,0 +1,51 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import com.google.common.collect.Iterators;
+import org.junit.Test;
+
+import java.net.NetworkInterface;
+import java.util.Collections;
+import java.util.Enumeration;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+
+public class LoopbackAddressTest {
+
+ @Test
+ public void get() throws Exception {
+ assertThat(LoopbackAddress.get()).isNotNull();
+ assertThat(LoopbackAddress.get().isLoopbackAddress()).isTrue();
+ assertThat(LoopbackAddress.get().getHostAddress()).isNotNull();
+ }
+
+ @Test
+ public void fail_to_get_loopback_address() throws Exception {
+ Enumeration<NetworkInterface> ifaces = Iterators.asEnumeration(Collections.<NetworkInterface>emptyList().iterator());
+ try {
+ LoopbackAddress.doGet(ifaces);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("Impossible to get a IPv4 loopback address");
+ }
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java b/server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java
new file mode 100644
index 00000000000..83841c79952
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java
@@ -0,0 +1,102 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.fest.assertions.Assertions;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+
+import static org.fest.assertions.Fail.fail;
+
+public class MinimumViableSystemTest {
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ /**
+ * Verifies that all checks can be verified without error.
+ * Test environment does not necessarily follows all checks.
+ */
+ @Test
+ public void check() throws Exception {
+ MinimumViableSystem mve = new MinimumViableSystem();
+
+ try {
+ mve.check();
+ // ok
+ } catch (MessageException e) {
+ // also ok. All other exceptions are errors.
+ }
+ }
+
+ @Test
+ public void checkJavaVersion() throws Exception {
+ MinimumViableSystem mve = new MinimumViableSystem();
+
+ // yes, sources are compiled with a supported Java version!
+ mve.checkJavaVersion();
+ mve.checkJavaVersion("1.6");
+
+ try {
+ mve.checkJavaVersion("1.9");
+ fail();
+ } catch (MessageException e) {
+ Assertions.assertThat(e).hasMessage("Supported versions of Java are 1.6, 1.7 and 1.8. Got 1.9.");
+ }
+ }
+
+ @Test
+ public void checkJavaOption() throws Exception {
+ String key = "MinimumViableEnvironmentTest.test.prop";
+ MinimumViableSystem mve = new MinimumViableSystem()
+ .setRequiredJavaOption(key, "true");
+
+ try {
+ System.setProperty(key, "false");
+ mve.checkJavaOptions();
+ fail();
+ } catch (MessageException e) {
+ Assertions.assertThat(e).hasMessage("JVM option '" + key + "' must be set to 'true'. Got 'false'");
+ }
+
+ System.setProperty(key, "true");
+ mve.checkJavaOptions();
+ // do not fail
+ }
+
+ @Test
+ public void checkWritableTempDir() throws Exception {
+ File dir = temp.newFolder();
+ MinimumViableSystem mve = new MinimumViableSystem();
+
+ mve.checkWritableDir(dir.getAbsolutePath());
+
+ dir.delete();
+ try {
+ mve.checkWritableDir(dir.getAbsolutePath());
+ fail();
+ } catch (IllegalStateException e) {
+ Assertions.assertThat(e).hasMessage("Temp directory is not writable: " + dir.getAbsolutePath());
+ }
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java
new file mode 100644
index 00000000000..09f6a597209
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java
@@ -0,0 +1,61 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.junit.Test;
+
+import java.net.ServerSocket;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class NetworkUtilsTest {
+
+
+ @Test
+ public void find_free_port() throws Exception {
+ int port = NetworkUtils.freePort();
+ assertThat(port).isGreaterThan(1024);
+ }
+
+ @Test
+ public void find_multiple_free_port() throws Exception {
+ int port1 = NetworkUtils.freePort();
+ int port2 = NetworkUtils.freePort();
+
+ assertThat(port1).isGreaterThan(1024);
+ assertThat(port2).isGreaterThan(1024);
+
+ assertThat(port1).isNotSameAs(port2);
+ }
+
+ @Test
+ public void find_multiple_free_non_adjacent_port() throws Exception {
+ int port1 = NetworkUtils.freePort();
+
+ ServerSocket socket = new ServerSocket(port1 + 1);
+
+ int port2 = NetworkUtils.freePort();
+
+ assertThat(port1).isGreaterThan(1024);
+ assertThat(port2).isGreaterThan(1024);
+
+ assertThat(port1).isNotSameAs(port2);
+ }
+} \ No newline at end of file
diff --git a/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java
new file mode 100644
index 00000000000..55bf8d679d1
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java
@@ -0,0 +1,224 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.apache.commons.io.FileUtils;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.rules.Timeout;
+import org.sonar.process.test.StandardProcess;
+
+import java.io.File;
+import java.util.Properties;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.mockito.Mockito.mock;
+
+public class ProcessEntryPointTest {
+
+ SystemExit exit = mock(SystemExit.class);
+
+ /**
+ * Safeguard
+ */
+ @Rule
+ public Timeout timeout = new Timeout(10000);
+
+ @Rule
+ public TemporaryFolder temp = new TemporaryFolder();
+
+ @Test
+ public void load_properties_from_file() throws Exception {
+ File propsFile = temp.newFile();
+ FileUtils.write(propsFile, "sonar.foo=bar");
+
+ ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(new String[]{propsFile.getAbsolutePath()});
+ assertThat(entryPoint.getProps().value("sonar.foo")).isEqualTo("bar");
+ }
+
+ @Test
+ public void test_initial_state() throws Exception {
+ Props props = new Props(new Properties());
+ ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
+
+ assertThat(entryPoint.getProps()).isSameAs(props);
+ assertThat(entryPoint.isReady()).isFalse();
+ assertThat(entryPoint.getState()).isEqualTo(State.INIT);
+
+ // do not fail
+ entryPoint.ping();
+ }
+
+ @Test
+ public void fail_to_launch_if_missing_monitor_properties() throws Exception {
+ Props props = new Props(new Properties());
+ ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
+
+ StandardProcess process = new StandardProcess();
+ try {
+ entryPoint.launch(process);
+ fail();
+ } catch (IllegalArgumentException e) {
+ assertThat(e).hasMessage("Missing property: process.key");
+ assertThat(process.getState()).isEqualTo(State.INIT);
+ }
+ }
+
+ @Test
+ public void fail_to_launch_multiple_times() throws Exception {
+ Props props = new Props(new Properties());
+ props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test");
+ props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true");
+ props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
+ ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
+
+ entryPoint.launch(new NoopProcess());
+ try {
+ entryPoint.launch(new NoopProcess());
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("Already started");
+ }
+ }
+
+ @Test
+ public void launch_then_request_graceful_termination() throws Exception {
+ Props props = new Props(new Properties());
+ props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test");
+ props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true");
+ props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
+ final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
+ final StandardProcess process = new StandardProcess();
+
+ Thread runner = new Thread() {
+ @Override
+ public void run() {
+ // starts and waits until terminated
+ entryPoint.launch(process);
+ }
+ };
+ runner.start();
+
+ while (process.getState() != State.STARTED) {
+ Thread.sleep(10L);
+ }
+
+ // requests for termination -> waits until down
+ // Should terminate before the timeout of 30s
+ entryPoint.terminate();
+
+ assertThat(process.getState()).isEqualTo(State.STOPPED);
+ }
+
+ @Test
+ public void autokill_if_no_pings() throws Exception {
+ Props props = new Props(new Properties());
+ props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test");
+ props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
+ props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_INTERVAL, "5");
+ props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_TIMEOUT, "1");
+ final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
+ final StandardProcess process = new StandardProcess();
+
+ entryPoint.launch(process);
+
+ assertThat(process.getState()).isEqualTo(State.STOPPED);
+ }
+
+ @Test
+ public void terminate_if_unexpected_shutdown() throws Exception {
+ Props props = new Props(new Properties());
+ props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo");
+ props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true");
+ props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
+ final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
+ final StandardProcess process = new StandardProcess();
+
+ Thread runner = new Thread() {
+ @Override
+ public void run() {
+ // starts and waits until terminated
+ entryPoint.launch(process);
+ }
+ };
+ runner.start();
+ while (process.getState() != State.STARTED) {
+ Thread.sleep(10L);
+ }
+
+ // emulate signal to shutdown process
+ entryPoint.getShutdownHook().start();
+ while (process.getState() != State.STOPPED) {
+ Thread.sleep(10L);
+ }
+ // exit before test timeout, ok !
+ }
+
+ @Test
+ public void terminate_if_startup_error() throws Exception {
+ Props props = new Props(new Properties());
+ props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo");
+ props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true");
+ props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
+ final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
+ final MonitoredProcess process = new StartupErrorProcess();
+
+ entryPoint.launch(process);
+ assertThat(entryPoint.getState()).isEqualTo(State.STOPPED);
+ }
+
+ private static class NoopProcess implements MonitoredProcess {
+
+ @Override
+ public void start() {
+
+ }
+
+ @Override
+ public void awaitTermination() {
+
+ }
+
+ @Override
+ public void terminate() {
+
+ }
+ }
+
+ private static class StartupErrorProcess implements MonitoredProcess {
+
+ @Override
+ public void start() {
+ throw new IllegalStateException("ERROR");
+ }
+
+ @Override
+ public void awaitTermination() {
+
+ }
+
+ @Override
+ public void terminate() {
+
+ }
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java
new file mode 100644
index 00000000000..6f93126516e
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java
@@ -0,0 +1,28 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.junit.Test;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class ProcessUtilsTest {
+
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/PropsTest.java b/server/sonar-process/src/test/java/org/sonar/process/PropsTest.java
new file mode 100644
index 00000000000..5d283b44f8f
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/PropsTest.java
@@ -0,0 +1,135 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.junit.Test;
+
+import java.util.Properties;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+
+public class PropsTest {
+
+ @Test
+ public void of() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("foo", "bar");
+ Props props = new Props(p);
+
+ assertThat(props.value("foo")).isEqualTo("bar");
+ assertThat(props.value("foo", "default value")).isEqualTo("bar");
+ assertThat(props.value("unknown")).isNull();
+ assertThat(props.value("unknown", "default value")).isEqualTo("default value");
+ }
+
+ @Test
+ public void intOf() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("foo", "33");
+ p.setProperty("blank", "");
+ Props props = new Props(p);
+
+ assertThat(props.valueAsInt("foo")).isEqualTo(33);
+ assertThat(props.valueAsInt("foo", 44)).isEqualTo(33);
+ assertThat(props.valueAsInt("blank")).isNull();
+ assertThat(props.valueAsInt("blank", 55)).isEqualTo(55);
+ assertThat(props.valueAsInt("unknown")).isNull();
+ assertThat(props.valueAsInt("unknown", 44)).isEqualTo(44);
+ }
+
+ @Test
+ public void intOf_not_integer() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("foo", "bar");
+ Props props = new Props(p);
+
+ try {
+ props.valueAsInt("foo");
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("Value of property foo is not an integer: bar");
+ }
+ }
+
+ @Test
+ public void booleanOf() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("foo", "True");
+ p.setProperty("bar", "false");
+ Props props = new Props(p);
+
+ assertThat(props.valueAsBoolean("foo")).isTrue();
+ assertThat(props.valueAsBoolean("bar")).isFalse();
+ assertThat(props.valueAsBoolean("unknown")).isFalse();
+ }
+
+ @Test
+ public void booleanOf_default_value() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("foo", "true");
+ p.setProperty("bar", "false");
+ Props props = new Props(p);
+
+ assertThat(props.valueAsBoolean("unset", false)).isFalse();
+ assertThat(props.valueAsBoolean("unset", true)).isTrue();
+ assertThat(props.valueAsBoolean("foo", false)).isTrue();
+ assertThat(props.valueAsBoolean("bar", true)).isFalse();
+ }
+
+ @Test
+ public void setDefault() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("foo", "foo_value");
+ Props props = new Props(p);
+ props.setDefault("foo", "foo_def");
+ props.setDefault("bar", "bar_def");
+
+ assertThat(props.value("foo")).isEqualTo("foo_value");
+ assertThat(props.value("bar")).isEqualTo("bar_def");
+ assertThat(props.value("other")).isNull();
+ }
+
+ @Test
+ public void set() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("foo", "old_foo");
+ Props props = new Props(p);
+ props.set("foo", "new_foo");
+ props.set("bar", "new_bar");
+
+ assertThat(props.value("foo")).isEqualTo("new_foo");
+ assertThat(props.value("bar")).isEqualTo("new_bar");
+ }
+
+ @Test
+ public void raw_properties() throws Exception {
+ Properties p = new Properties();
+ p.setProperty("encrypted_prop", "{aes}abcde");
+ p.setProperty("clear_prop", "foo");
+ Props props = new Props(p);
+
+ assertThat(props.rawProperties()).hasSize(2);
+ // do not decrypt
+ assertThat(props.rawProperties().get("encrypted_prop")).isEqualTo("{aes}abcde");
+ assertThat(props.rawProperties().get("clear_prop")).isEqualTo("foo");
+
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java b/server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java
new file mode 100644
index 00000000000..e02b07cf555
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java
@@ -0,0 +1,56 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process;
+
+import org.junit.Test;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.fest.assertions.Assertions.assertThat;
+
+public class SystemExitTest {
+
+ @Test
+ public void do_not_exit_if_in_shutdown_hook() throws Exception {
+ SystemExit systemExit = new SystemExit();
+
+ systemExit.setInShutdownHook();
+ assertThat(systemExit.isInShutdownHook()).isTrue();
+
+ systemExit.exit(0);
+ // still there
+ }
+
+ @Test
+ public void exit_if_not_in_shutdown_hook() throws Exception {
+ final AtomicInteger got = new AtomicInteger();
+ SystemExit systemExit = new SystemExit() {
+ @Override
+ void doExit(int code) {
+ got.set(code);
+ }
+ };
+
+ assertThat(systemExit.isInShutdownHook()).isFalse();
+ systemExit.exit(1);
+
+ assertThat(got.get()).isEqualTo(1);
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java b/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java
new file mode 100644
index 00000000000..307abed6d0b
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java
@@ -0,0 +1,116 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.test;
+
+import org.apache.commons.io.FileUtils;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Server;
+import org.eclipse.jetty.server.handler.AbstractHandler;
+import org.eclipse.jetty.server.handler.ContextHandler;
+import org.sonar.process.MonitoredProcess;
+import org.sonar.process.ProcessEntryPoint;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * Http server used for testing (see MonitorTest). It accepts HTTP commands /ping and /kill to hardly exit.
+ * It also pushes status to temp files, so test can verify what was really done (when server went ready state and
+ * if it was gracefully terminated)
+ */
+public class HttpProcess implements MonitoredProcess {
+
+ private final Server server;
+ // temp dir is specific to this process
+ private final File tempDir = new File(System.getProperty("java.io.tmpdir"));
+
+ public HttpProcess(int httpPort) {
+ server = new Server(httpPort);
+ }
+
+ @Override
+ public void start() {
+ writeTimeToFile("startingAt");
+ ContextHandler context = new ContextHandler();
+ context.setContextPath("/");
+ context.setClassLoader(Thread.currentThread().getContextClassLoader());
+ server.setHandler(context);
+ context.setHandler(new AbstractHandler() {
+ @Override
+ public void handle(String target, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException {
+ if ("/ping".equals(target)) {
+ request.setHandled(true);
+ httpServletResponse.getWriter().print("ping");
+ } else if ("/kill".equals(target)) {
+ writeTimeToFile("killedAt");
+ System.exit(0);
+ }
+ }
+ });
+ try {
+ server.start();
+ while (!server.isStarted()) {
+ Thread.sleep(100L);
+ }
+ writeTimeToFile("readyAt");
+
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to start Jetty", e);
+ }
+ }
+
+ @Override
+ public void awaitTermination() {
+ try {
+ server.join();
+ } catch (InterruptedException ignore) {
+
+ }
+ }
+
+ @Override
+ public void terminate() {
+ try {
+ if (!server.isStopped()) {
+ server.stop();
+ writeTimeToFile("terminatedAt");
+ }
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to stop Jetty", e);
+ }
+ }
+
+ private void writeTimeToFile(String filename) {
+ try {
+ FileUtils.write(new File(tempDir, filename), String.valueOf(System.currentTimeMillis()));
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public static void main(String[] args) {
+ ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
+ entryPoint.launch(new HttpProcess(entryPoint.getProps().valueAsInt("httpPort")));
+ }
+}
diff --git a/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java b/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java
new file mode 100644
index 00000000000..121784f633b
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java
@@ -0,0 +1,81 @@
+/*
+ * SonarQube, open source software quality management tool.
+ * Copyright (C) 2008-2014 SonarSource
+ * mailto:contact AT sonarsource DOT com
+ *
+ * SonarQube 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.
+ *
+ * SonarQube 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.process.test;
+
+import org.sonar.process.MonitoredProcess;
+import org.sonar.process.ProcessEntryPoint;
+import org.sonar.process.State;
+
+public class StandardProcess implements MonitoredProcess {
+
+ private State state = State.INIT;
+
+ private final Thread daemon = new Thread() {
+ @Override
+ public void run() {
+ try {
+ while (true) {
+ Thread.sleep(100L);
+ }
+ } catch (InterruptedException e) {
+ // return
+ }
+ }
+ };
+
+ /**
+ * Blocks until started()
+ */
+ @Override
+ public void start() {
+ state = State.STARTING;
+ daemon.start();
+ state = State.STARTED;
+ }
+
+ @Override
+ public void awaitTermination() {
+ try {
+ daemon.join();
+ } catch (InterruptedException e) {
+ // interrupted by call to terminate()
+ }
+ }
+
+ /**
+ * Blocks until stopped
+ */
+ @Override
+ public void terminate() {
+ state = State.STOPPING;
+ daemon.interrupt();
+ state = State.STOPPED;
+ }
+
+ public State getState() {
+ return state;
+ }
+
+ public static void main(String[] args) {
+ ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
+ entryPoint.launch(new StandardProcess());
+ System.exit(0);
+ }
+}
diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt
new file mode 100644
index 00000000000..65b98c522da
--- /dev/null
+++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt
@@ -0,0 +1 @@
+0PZz+G+f8mjr3sPn4+AhHg== \ No newline at end of file
diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt
new file mode 100644
index 00000000000..b33e179e5c8
--- /dev/null
+++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt
@@ -0,0 +1 @@
+badbadbad== \ No newline at end of file
diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt
new file mode 100644
index 00000000000..ab83e4adc03
--- /dev/null
+++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt
@@ -0,0 +1,3 @@
+
+ 0PZz+G+f8mjr3sPn4+AhHg==
+
diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt
new file mode 100644
index 00000000000..23f5ecf5104
--- /dev/null
+++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt
@@ -0,0 +1 @@
+IBxEUxZ41c8XTxyaah1Qlg== \ No newline at end of file
diff --git a/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml b/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml
new file mode 100644
index 00000000000..298193e01fa
--- /dev/null
+++ b/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml
@@ -0,0 +1 @@
+<configuration/>
diff --git a/server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties b/server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties
new file mode 100644
index 00000000000..1577a214b3b
--- /dev/null
+++ b/server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties
@@ -0,0 +1,212 @@
+# This file must contain only ISO 8859-1 characters
+# see http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Properties.html#load(java.io.InputStream)
+#
+# To use an environment variable, use the following syntax : ${env:NAME_OF_ENV_VARIABLE}
+# For example:
+# sonar.jdbc.url= ${env:SONAR_JDBC_URL}
+#
+#
+# See also the file conf/wrapper.conf for JVM advanced settings
+
+
+
+#--------------------------------------------------------------------------------------------------
+# DATABASE
+#
+# IMPORTANT: the embedded H2 database is used by default. It is recommended for tests only.
+# Please use a production-ready database. Supported databases are MySQL, Oracle, PostgreSQL
+# and Microsoft SQLServer.
+
+# Permissions to create tables, indices and triggers must be granted to JDBC user.
+# The schema must be created first.
+sonar.jdbc.username=sonar
+sonar.jdbc.password=sonar
+
+#----- Embedded database H2
+# Note: it does not accept connections from remote hosts, so the
+# SonarQube server and the maven plugin must be executed on the same host.
+
+# Comment the following line to deactivate the default embedded database.
+sonar.jdbc.url=jdbc:h2:tcp://localhost:9092/sonar
+
+# directory containing H2 database files. By default it's the /data directory in the SonarQube installation.
+#sonar.embeddedDatabase.dataDir=
+# H2 embedded database server listening port, defaults to 9092
+#sonar.embeddedDatabase.port=9092
+
+
+#----- MySQL 5.x
+# Comment the embedded database and uncomment the following line to use MySQL
+#sonar.jdbc.url=jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true
+
+
+#----- Oracle 10g/11g
+# To connect to Oracle database:
+#
+# - It's recommended to use the latest version of the JDBC driver (ojdbc6.jar).
+# Download it in http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html
+# - Copy the driver to the directory extensions/jdbc-driver/oracle/
+# - If you need to set the schema, please refer to http://jira.codehaus.org/browse/SONAR-5000
+# - Comment the embedded database and uncomment the following line:
+#sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE
+
+
+#----- PostgreSQL 8.x/9.x
+# Comment the embedded database and uncomment the following property to use PostgreSQL.
+# If you don't use the schema named "public", please refer to http://jira.codehaus.org/browse/SONAR-5000
+#sonar.jdbc.url=jdbc:postgresql://localhost/sonar
+
+
+#----- Microsoft SQLServer
+# The Jtds open source driver is available in extensions/jdbc-driver/mssql. More details on http://jtds.sourceforge.net
+#sonar.jdbc.url=jdbc:jtds:sqlserver://localhost/sonar;SelectMethod=Cursor
+
+
+#----- Connection pool settings
+sonar.jdbc.maxActive=20
+sonar.jdbc.maxIdle=5
+sonar.jdbc.minIdle=2
+sonar.jdbc.maxWait=5000
+sonar.jdbc.minEvictableIdleTimeMillis=600000
+sonar.jdbc.timeBetweenEvictionRunsMillis=30000
+
+
+
+#--------------------------------------------------------------------------------------------------
+# WEB SERVER
+
+# Binding IP address. For servers with more than one IP address, this property specifies which
+# address will be used for listening on the specified ports.
+# By default, ports will be used on all IP addresses associated with the server.
+#sonar.web.host=0.0.0.0
+
+# Web context. When set, it must start with forward slash (for example /sonarqube).
+# The default value is root context (empty value).
+#sonar.web.context=
+
+# TCP port for incoming HTTP connections. Disabled when value is -1.
+#sonar.web.port=9000
+
+# TCP port for incoming HTTPS connections. Disabled when value is -1 (default).
+#sonar.web.https.port=-1
+
+# HTTPS - the alias used to for the server certificate in the keystore.
+# If not specified the first key read in the keystore is used.
+#sonar.web.https.keyAlias=
+
+# HTTPS - the password used to access the server certificate from the
+# specified keystore file. The default value is "changeit".
+#sonar.web.https.keyPass=changeit
+
+# HTTPS - the pathname of the keystore file where is stored the server certificate.
+# By default, the pathname is the file ".keystore" in the user home.
+# If keystoreType doesn't need a file use empty value.
+#sonar.web.https.keystoreFile=
+
+# HTTPS - the password used to access the specified keystore file. The default
+# value is the value of sonar.web.https.keyPass.
+#sonar.web.https.keystorePass=
+
+# HTTPS - the type of keystore file to be used for the server certificate.
+# The default value is JKS (Java KeyStore).
+#sonar.web.https.keystoreType=JKS
+
+# HTTPS - the name of the keystore provider to be used for the server certificate.
+# If not specified, the list of registered providers is traversed in preference order
+# and the first provider that supports the keystore type is used (see sonar.web.https.keystoreType).
+#sonar.web.https.keystoreProvider=
+
+# HTTPS - the pathname of the truststore file which contains trusted certificate authorities.
+# By default, this would be the cacerts file in your JRE.
+# If truststoreFile doesn't need a file use empty value.
+#sonar.web.https.truststoreFile=
+
+# HTTPS - the password used to access the specified truststore file.
+#sonar.web.https.truststorePass=
+
+# HTTPS - the type of truststore file to be used.
+# The default value is JKS (Java KeyStore).
+#sonar.web.https.truststoreType=JKS
+
+# HTTPS - the name of the truststore provider to be used for the server certificate.
+# If not specified, the list of registered providers is traversed in preference order
+# and the first provider that supports the truststore type is used (see sonar.web.https.truststoreType).
+#sonar.web.https.truststoreProvider=
+
+# HTTPS - whether to enable client certificate authentication.
+# The default is false (client certificates disabled).
+# Other possible values are 'want' (certificates will be requested, but not required),
+# and 'true' (certificates are required).
+#sonar.web.https.clientAuth=false
+
+# The maximum number of connections that the server will accept and process at any given time.
+# When this number has been reached, the server will not accept any more connections until
+# the number of connections falls below this value. The operating system may still accept connections
+# based on the sonar.web.connections.acceptCount property. The default value is 50 for each
+# enabled connector.
+#sonar.web.http.maxThreads=50
+#sonar.web.https.maxThreads=50
+
+# The minimum number of threads always kept running. The default value is 5 for each
+# enabled connector.
+#sonar.web.http.minThreads=5
+#sonar.web.https.minThreads=5
+
+# The maximum queue length for incoming connection requests when all possible request processing
+# threads are in use. Any requests received when the queue is full will be refused.
+# The default value is 25 for each enabled connector.
+#sonar.web.http.acceptCount=25
+#sonar.web.https.acceptCount=25
+
+# Access logs are generated in the file logs/access.log. This file is rolled over when it's 5Mb.
+# An archive of 3 files is kept in the same directory.
+# Access logs are enabled by default.
+#sonar.web.accessLogs.enable=true
+
+# TCP port for incoming AJP connections. Disabled when value is -1.
+# sonar.ajp.port=9009
+
+
+
+#--------------------------------------------------------------------------------------------------
+# UPDATE CENTER
+
+# The Update Center requires an internet connection to request http://update.sonarsource.org
+# It is enabled by default.
+#sonar.updatecenter.activate=true
+
+# HTTP proxy (default none)
+#http.proxyHost=
+#http.proxyPort=
+
+# NT domain name if NTLM proxy is used
+#http.auth.ntlm.domain=
+
+# SOCKS proxy (default none)
+#socksProxyHost=
+#socksProxyPort=
+
+# proxy authentication. The 2 following properties are used for HTTP and SOCKS proxies.
+#http.proxyUser=
+#http.proxyPassword=
+
+
+#--------------------------------------------------------------------------------------------------
+# NOTIFICATIONS
+
+# Delay in seconds between processing of notification queue. Default is 60.
+#sonar.notifications.delay=60
+
+
+#--------------------------------------------------------------------------------------------------
+# PROFILING
+# Level of information displayed in the logs: NONE (default), BASIC (functional information) and FULL (functional and technical details)
+#sonar.log.profilingLevel=NONE
+
+
+#--------------------------------------------------------------------------------------------------
+# DEVELOPMENT MODE
+# Only for debugging
+
+# Set to true to apply Ruby on Rails code changes on the fly
+#sonar.rails.dev=false
diff --git a/server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties b/server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties
new file mode 100644
index 00000000000..5c06e58a32e
--- /dev/null
+++ b/server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties
@@ -0,0 +1,3 @@
+hello: world
+foo=bar
+java.io.tmpdir=/should/be/overridden
diff --git a/server/sonar-process/src/test/resources/sonar-dummy-app.jar b/server/sonar-process/src/test/resources/sonar-dummy-app.jar
new file mode 100644
index 00000000000..6dfd458329a
--- /dev/null
+++ b/server/sonar-process/src/test/resources/sonar-dummy-app.jar
Binary files differ
diff --git a/server/sonar-process/test-jar-with-dependencies.xml b/server/sonar-process/test-jar-with-dependencies.xml
new file mode 100644
index 00000000000..832c66cc0be
--- /dev/null
+++ b/server/sonar-process/test-jar-with-dependencies.xml
@@ -0,0 +1,19 @@
+<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd">
+ <id>test-jar-with-dependencies</id>
+ <formats>
+ <format>jar</format>
+ </formats>
+ <includeBaseDirectory>false</includeBaseDirectory>
+ <dependencySets>
+ <dependencySet>
+ <outputDirectory>/</outputDirectory>
+ <useProjectArtifact>true</useProjectArtifact>
+ <!-- we're creating the test-jar as an attachement -->
+ <useProjectAttachments>true</useProjectAttachments>
+ <unpack>true</unpack>
+ <scope>test</scope>
+ </dependencySet>
+ </dependencySets>
+</assembly>
diff --git a/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java
index 1abf733b66b..6e5a366ff64 100644
--- a/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java
+++ b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java
@@ -21,15 +21,14 @@ package org.sonar.search;
import org.apache.commons.lang.StringUtils;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus;
-import org.elasticsearch.common.annotations.VisibleForTesting;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.NodeBuilder;
import org.slf4j.LoggerFactory;
-import org.sonar.process.ConfigurationUtils;
import org.sonar.process.MinimumViableSystem;
import org.sonar.process.MonitoredProcess;
+import org.sonar.process.ProcessEntryPoint;
import org.sonar.process.ProcessLogging;
import org.sonar.process.Props;
import org.sonar.search.script.ListUpdate;
@@ -40,7 +39,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
-public class SearchServer extends MonitoredProcess {
+public class SearchServer implements MonitoredProcess {
public static final String SONAR_NODE_NAME = "sonar.node.name";
public static final String ES_PORT_PROPERTY = "sonar.search.port";
@@ -55,27 +54,13 @@ public class SearchServer extends MonitoredProcess {
private static final Integer MINIMUM_INDEX_REPLICATION = 1;
private final Set<String> nodes = new HashSet<String>();
- private final boolean isBlocking;
-
- private Node node;
+ private final Props props;
private final Object lock = new Object();
- @VisibleForTesting
- public SearchServer(final Props props, boolean monitored, boolean blocking) {
- super(props, monitored);
-
- this.isBlocking = blocking;
- new MinimumViableSystem().check();
-
- String esNodesInets = props.value(ES_CLUSTER_INET);
- if (StringUtils.isNotEmpty(esNodesInets)) {
- Collections.addAll(nodes, esNodesInets.split(","));
- }
- }
+ private Node node;
public SearchServer(Props props) {
- super(props);
- this.isBlocking = true;
+ this.props = props;
new MinimumViableSystem().check();
String esNodesInets = props.value(ES_CLUSTER_INET);
@@ -85,18 +70,8 @@ public class SearchServer extends MonitoredProcess {
}
@Override
- protected boolean doIsReady() {
- return node.client().admin().cluster().prepareHealth()
- .setWaitForYellowStatus()
- .setTimeout(TimeValue.timeValueSeconds(3L))
- .get()
- .getStatus() != ClusterHealthStatus.RED;
- }
-
- @Override
- protected void doStart() {
+ public void start() {
synchronized (lock) {
-
Integer port = props.valueAsInt(ES_PORT_PROPERTY);
String clusterName = props.value(ES_CLUSTER_PROPERTY);
@@ -169,17 +144,25 @@ public class SearchServer extends MonitoredProcess {
.addMapping("_default_", "{\"dynamic\": \"strict\"}")
.get();
}
+ }
- if (isBlocking) {
- while (node != null && !node.isClosed()) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- // Ignore
- }
+ boolean isReady() {
+ return node.client().admin().cluster().prepareHealth()
+ .setWaitForYellowStatus()
+ .setTimeout(TimeValue.timeValueSeconds(3L))
+ .get()
+ .getStatus() != ClusterHealthStatus.RED;
+ }
+
+ @Override
+ public void awaitTermination() {
+ while (node != null && !node.isClosed()) {
+ try {
+ Thread.sleep(200L);
+ } catch (InterruptedException e) {
+ // Ignore
}
}
-
}
private void initAnalysis(ImmutableSettings.Builder esSettings) {
@@ -188,40 +171,40 @@ public class SearchServer extends MonitoredProcess {
// Disallow dynamic mapping (too expensive)
.put("index.mapper.dynamic", false)
- // Sortable text analyzer
+ // Sortable text analyzer
.put("index.analysis.analyzer.sortable.type", "custom")
.put("index.analysis.analyzer.sortable.tokenizer", "keyword")
.putArray("index.analysis.analyzer.sortable.filter", "trim", "lowercase", "truncate")
- // Edge NGram index-analyzer
+ // Edge NGram index-analyzer
.put("index.analysis.analyzer.index_grams.type", "custom")
.put("index.analysis.analyzer.index_grams.tokenizer", "whitespace")
.putArray("index.analysis.analyzer.index_grams.filter", "trim", "lowercase", "gram_filter")
- // Edge NGram search-analyzer
+ // Edge NGram search-analyzer
.put("index.analysis.analyzer.search_grams.type", "custom")
.put("index.analysis.analyzer.search_grams.tokenizer", "whitespace")
.putArray("index.analysis.analyzer.search_grams.filter", "trim", "lowercase")
- // Word index-analyzer
+ // Word index-analyzer
.put("index.analysis.analyzer.index_words.type", "custom")
.put("index.analysis.analyzer.index_words.tokenizer", "standard")
.putArray("index.analysis.analyzer.index_words.filter",
"standard", "word_filter", "lowercase", "stop", "asciifolding", "porter_stem")
- // Word search-analyzer
+ // Word search-analyzer
.put("index.analysis.analyzer.search_words.type", "custom")
.put("index.analysis.analyzer.search_words.tokenizer", "standard")
.putArray("index.analysis.analyzer.search_words.filter",
"standard", "lowercase", "stop", "asciifolding", "porter_stem")
- // Edge NGram filter
+ // Edge NGram filter
.put("index.analysis.filter.gram_filter.type", "edgeNGram")
.put("index.analysis.filter.gram_filter.min_gram", 2)
.put("index.analysis.filter.gram_filter.max_gram", 15)
.putArray("index.analysis.filter.gram_filter.token_chars", "letter", "digit", "punctuation", "symbol")
- // Word filter
+ // Word filter
.put("index.analysis.filter.word_filter.type", "word_delimiter")
.put("index.analysis.filter.word_filter.generate_word_parts", true)
.put("index.analysis.filter.word_filter.catenate_words", true)
@@ -232,7 +215,7 @@ public class SearchServer extends MonitoredProcess {
.put("index.analysis.filter.word_filter.split_on_numerics", true)
.put("index.analysis.filter.word_filter.stem_english_possessive", true)
- // Path Analyzer
+ // Path Analyzer
.put("index.analysis.analyzer.path_analyzer.type", "custom")
.put("index.analysis.analyzer.path_analyzer.tokenizer", "path_hierarchy");
@@ -267,18 +250,18 @@ public class SearchServer extends MonitoredProcess {
}
@Override
- protected void doTerminate() {
+ public void terminate() {
synchronized (lock) {
- if (node != null && !node.isClosed()) {
+ if (!node.isClosed()) {
node.close();
- node = null;
}
}
}
public static void main(String... args) {
- Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args);
- new ProcessLogging().configure(props, "/org/sonar/search/logback.xml");
- new SearchServer(props).start();
+ ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
+ new ProcessLogging().configure(entryPoint.getProps(), "/org/sonar/search/logback.xml");
+ SearchServer searchServer = new SearchServer(entryPoint.getProps());
+ entryPoint.launch(searchServer);
}
}
diff --git a/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java b/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java
index bdf54bb5187..54fb02435e9 100644
--- a/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java
+++ b/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java
@@ -26,16 +26,17 @@ import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.InetSocketTransportAddress;
import org.junit.After;
import org.junit.Before;
+import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.sonar.process.JmxUtils;
-import org.sonar.process.MonitoredProcess;
import org.sonar.process.Props;
import javax.management.InstanceNotFoundException;
import javax.management.MBeanRegistrationException;
import javax.management.MBeanServer;
+
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.net.ServerSocket;
@@ -44,6 +45,7 @@ import java.util.Properties;
import static org.fest.assertions.Assertions.assertThat;
import static org.junit.Assert.fail;
+@Ignore
public class SearchServerTest {
@Rule
@@ -81,7 +83,6 @@ public class SearchServerTest {
@Test
public void server_fail_to_start() throws Exception {
Properties properties = new Properties();
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES");
searchServer = new SearchServer(new Props(properties));
new Thread(new Runnable() {
@@ -107,7 +108,6 @@ public class SearchServerTest {
@Test
public void can_connect() throws Exception {
Properties properties = new Properties();
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES");
properties.setProperty(SearchServer.SONAR_PATH_DATA, temp.newFolder().getAbsolutePath());
properties.setProperty(SearchServer.SONAR_PATH_TEMP, temp.newFolder().getAbsolutePath());
properties.setProperty(SearchServer.SONAR_PATH_LOG, temp.newFolder().getAbsolutePath());
diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java b/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java
index 2356ff340e8..a7146eda913 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java
@@ -19,22 +19,32 @@
*/
package org.sonar.server.app;
+import com.google.common.base.Throwables;
+import com.google.common.util.concurrent.Uninterruptibles;
+import org.apache.catalina.Container;
+import org.apache.catalina.Executor;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.LifecycleState;
+import org.apache.catalina.Server;
+import org.apache.catalina.Service;
+import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.io.FileUtils;
import org.slf4j.LoggerFactory;
-import org.sonar.process.ProcessUtils;
import org.sonar.process.Props;
-import org.sonar.process.Terminable;
import java.io.File;
+import java.util.concurrent.TimeUnit;
-class EmbeddedTomcat implements Terminable {
+class EmbeddedTomcat {
private final Props props;
private Tomcat tomcat = null;
private Thread hook = null;
- private boolean ready = false;
+ private volatile StandardContext webappContext;
EmbeddedTomcat(Props props) {
this.props = props;
@@ -62,45 +72,52 @@ class EmbeddedTomcat implements Terminable {
tomcat.getHost().setDeployOnStartup(true);
Logging.configure(tomcat, props);
Connectors.configure(tomcat, props);
- StandardContext webappContext = Webapp.configure(tomcat, props);
- ProcessUtils.addSelfShutdownHook(this);
+ webappContext = Webapp.configure(tomcat, props);
tomcat.start();
+ waitForWebappReady();
- if (webappContext.getState().isAvailable()) {
- ready = true;
- tomcat.getServer().await();
- }
} catch (Exception e) {
- throw new IllegalStateException("Fail to start web server", e);
- } finally {
- // Failed to start or received a shutdown command (should never occur as shutdown port is disabled)
- terminate();
+ Throwables.propagate(e);
}
}
- private File tomcatBasedir() {
- return new File(props.value("sonar.path.temp"), "tc");
+ private void waitForWebappReady() {
+ while (true) {
+ switch (webappContext.getState()) {
+ case NEW:
+ case INITIALIZING:
+ case INITIALIZED:
+ case STARTING_PREP:
+ case STARTING:
+ Uninterruptibles.sleepUninterruptibly(300L, TimeUnit.MILLISECONDS);
+ break;
+ case STARTED:
+ // ok
+ return;
+ default:
+ // problem, stopped or failed
+ throw new IllegalStateException("YYY Webapp did not start");
+ }
+ }
}
- boolean isReady() {
- return ready && tomcat != null;
+ private File tomcatBasedir() {
+ return new File(props.value("sonar.path.temp"), "tc");
}
- @Override
- public void terminate() {
- if (tomcat != null) {
- synchronized (tomcat) {
- if (tomcat.getServer().getState().isAvailable()) {
- try {
- tomcat.stop();
- tomcat.destroy();
- } catch (Exception e) {
- LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to stop web service", e);
- }
- }
+ void terminate() {
+ if (tomcat.getServer().getState().isAvailable()) {
+ try {
+ tomcat.stop();
+ tomcat.destroy();
+ } catch (Exception e) {
+ LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to stop web server", e);
}
}
- ready = false;
FileUtils.deleteQuietly(tomcatBasedir());
}
+
+ void awaitTermination() {
+ tomcat.getServer().await();
+ }
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java b/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java
index d5bc4fde7de..94bc1214f0f 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java
@@ -19,18 +19,16 @@
*/
package org.sonar.server.app;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.ConfigurationUtils;
import org.sonar.process.MinimumViableSystem;
import org.sonar.process.MonitoredProcess;
+import org.sonar.process.ProcessEntryPoint;
import org.sonar.process.Props;
-public class WebServer extends MonitoredProcess {
+public class WebServer implements MonitoredProcess {
private final EmbeddedTomcat tomcat;
WebServer(Props props) throws Exception {
- super(props);
new MinimumViableSystem()
.setRequiredJavaOption("file.encoding", "UTF-8")
.check();
@@ -38,32 +36,27 @@ public class WebServer extends MonitoredProcess {
}
@Override
- protected void doStart() {
- try {
- tomcat.start();
- } catch (Exception e) {
- LoggerFactory.getLogger(getClass()).error("TC error", e);
- } finally {
- terminate();
- }
+ public void start() {
+ tomcat.start();
}
@Override
- protected void doTerminate() {
+ public void terminate() {
tomcat.terminate();
}
@Override
- protected boolean doIsReady() {
- return tomcat.isReady();
+ public void awaitTermination() {
+ tomcat.awaitTermination();
}
/**
* Can't be started as is. Needs to be bootstrapped by sonar-application
*/
public static void main(String[] args) throws Exception {
- Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args);
- Logging.init(props);
- new WebServer(props).start();
+ ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
+ Logging.init(entryPoint.getProps());
+ WebServer server = new WebServer(entryPoint.getProps());
+ entryPoint.launch(server);
}
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java b/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java
index f959faed106..8445efb22be 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java
@@ -64,7 +64,6 @@ class Webapp {
String key = entry.getKey().toString();
context.addParameter(key, entry.getValue().toString());
}
-
return context;
} catch (Exception e) {
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java b/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java
index 46bc0426333..33efbcd7e06 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java
@@ -19,7 +19,7 @@
*/
package org.sonar.server.platform;
-import org.slf4j.LoggerFactory;
+import com.google.common.base.Throwables;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
@@ -46,9 +46,9 @@ public final class PlatformServletContextListener implements ServletContextListe
// - server does not stop if webapp fails at startup
// - the second listener for jruby on rails is started even if this listener fails. It generates
// unexpected errors
- LoggerFactory.getLogger(getClass()).error("Fail to start server", t);
+ // LoggerFactory.getLogger(getClass()).error("Fail to start server", t);
stopQuietly();
- throw new IllegalStateException("Fail to start webapp", t);
+ throw Throwables.propagate(t);
}
}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java b/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java
index 3884439e166..5abc8a0eaef 100644
--- a/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java
+++ b/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java
@@ -211,9 +211,11 @@ public class ServerPluginJarsInstaller {
private void deploy(DefaultPluginMetadata plugin) {
LOG.info("Deploy plugin {}", Joiner.on(" / ").skipNulls().join(plugin.getName(), plugin.getVersion(), plugin.getImplementationBuild()));
- Preconditions.checkState(plugin.isCompatibleWith(server.getVersion()),
- "Plugin %s needs a more recent version of SonarQube than %s. At least %s is expected",
- plugin.getKey(), server.getVersion(), plugin.getSonarVersion());
+ if (!plugin.isCompatibleWith(server.getVersion())) {
+ throw MessageException.of(String.format(
+ "Plugin %s needs a more recent version of SonarQube than %s. At least %s is expected",
+ plugin.getKey(), server.getVersion(), plugin.getSonarVersion()));
+ }
try {
File pluginDeployDir = new File(fs.getDeployedPluginsDir(), plugin.getKey());
diff --git a/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java b/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java
index be40c087f88..90cce6fb4ed 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java
@@ -85,7 +85,7 @@ public class ServerPluginJarsInstallerTest {
jarsInstaller.install();
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1);
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1);
assertThat(new File(pluginsDir, "foo-plugin-1.0.jar")).exists().isFile();
PluginMetadata plugin = jarsInstaller.getMetadata("foo");
assertThat(plugin.getName()).isEqualTo("Foo");
@@ -101,7 +101,7 @@ public class ServerPluginJarsInstallerTest {
jarsInstaller.install();
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).isEmpty();
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).isEmpty();
}
@Test
@@ -115,7 +115,7 @@ public class ServerPluginJarsInstallerTest {
jarsInstaller.install();
// do not copy foo 1.0
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(2);
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(2);
assertThat(new File(pluginsDir, "foo-plugin-2.0.jar")).exists().isFile();
assertThat(new File(pluginsDir, "bar-plugin-1.0.jar")).exists().isFile();
PluginMetadata plugin = jarsInstaller.getMetadata("foo");
@@ -138,7 +138,7 @@ public class ServerPluginJarsInstallerTest {
assertThat(plugin.isUseChildFirstClassLoader()).isFalse();
// check that the file is still present in extensions/plugins
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1);
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1);
assertThat(new File(pluginsDir, "foo-plugin-1.0.jar")).exists().isFile();
}
@@ -151,13 +151,13 @@ public class ServerPluginJarsInstallerTest {
// nothing to install but keep the file
assertThat(jarsInstaller.getMetadata()).isEmpty();
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1);
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1);
assertThat(new File(pluginsDir, "not-a-plugin.jar")).exists().isFile();
}
@Test
public void fail_if_plugin_requires_greater_SQ_version() throws Exception {
- exception.expect(IllegalStateException.class);
+ exception.expect(MessageException.class);
exception.expectMessage("Plugin switchoffviolations needs a more recent version of SonarQube than 2.0. At least 2.5 is expected");
when(upgradeStatus.isFreshInstall()).thenReturn(false);
@@ -174,8 +174,8 @@ public class ServerPluginJarsInstallerTest {
jarsInstaller.install();
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1);
- assertThat(FileUtils.listFiles(downloadsDir, new String[]{"jar"}, false)).isEmpty();
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1);
+ assertThat(FileUtils.listFiles(downloadsDir, new String[] {"jar"}, false)).isEmpty();
assertThat(new File(pluginsDir, "foo-plugin-1.0.jar")).exists().isFile();
}
@@ -187,8 +187,8 @@ public class ServerPluginJarsInstallerTest {
jarsInstaller.install();
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1);
- assertThat(FileUtils.listFiles(downloadsDir, new String[]{"jar"}, false)).isEmpty();
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1);
+ assertThat(FileUtils.listFiles(downloadsDir, new String[] {"jar"}, false)).isEmpty();
assertThat(new File(pluginsDir, "foo-plugin-2.0.jar")).exists().isFile();
}
@@ -205,8 +205,8 @@ public class ServerPluginJarsInstallerTest {
PluginMetadata plugin = jarsInstaller.getMetadata("foo");
assertThat(plugin).isNotNull();
assertThat(plugin.getVersion()).isEqualTo("2.0");
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1);
- assertThat(FileUtils.listFiles(downloadsDir, new String[]{"jar"}, false)).isEmpty();
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1);
+ assertThat(FileUtils.listFiles(downloadsDir, new String[] {"jar"}, false)).isEmpty();
File installed = new File(pluginsDir, "foo-plugin-1.0.jar");
assertThat(installed).exists().isFile();
}
@@ -218,7 +218,7 @@ public class ServerPluginJarsInstallerTest {
jarsInstaller.install();
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).isEmpty();
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).isEmpty();
assertThat(trashDir).doesNotExist();
}
@@ -247,8 +247,8 @@ public class ServerPluginJarsInstallerTest {
jarsInstaller.install();
jarsInstaller.uninstall("foo");
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).isEmpty();
- assertThat(FileUtils.listFiles(trashDir, new String[]{"jar"}, false)).hasSize(1);
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).isEmpty();
+ assertThat(FileUtils.listFiles(trashDir, new String[] {"jar"}, false)).hasSize(1);
assertThat(jarsInstaller.getUninstalls()).containsOnly("foo-plugin-1.0.jar");
}
@@ -261,8 +261,8 @@ public class ServerPluginJarsInstallerTest {
jarsInstaller.uninstall("foo");
jarsInstaller.cancelUninstalls();
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1);
- assertThat(FileUtils.listFiles(trashDir, new String[]{"jar"}, false)).hasSize(0);
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1);
+ assertThat(FileUtils.listFiles(trashDir, new String[] {"jar"}, false)).hasSize(0);
assertThat(jarsInstaller.getUninstalls()).isEmpty();
}
@@ -274,10 +274,10 @@ public class ServerPluginJarsInstallerTest {
jarsInstaller.install();
// do not deploy in extensions/plugins
- assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(0);
+ assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(0);
// do not remove from lib/core-plugins
- assertThat(FileUtils.listFiles(coreDir, new String[]{"jar"}, false)).hasSize(1);
+ assertThat(FileUtils.listFiles(coreDir, new String[] {"jar"}, false)).hasSize(1);
PluginMetadata plugin = jarsInstaller.getMetadata("foo");
assertThat(plugin).isNotNull();
diff --git a/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java
index e92fd5b936f..8270a46f06e 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java
@@ -61,10 +61,9 @@ public class BaseIndexTest {
Properties properties = new Properties();
properties.setProperty(IndexProperties.CLUSTER_NAME, clusterName);
properties.setProperty(IndexProperties.NODE_PORT, clusterPort.toString());
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES");
properties.setProperty(SearchServer.SONAR_PATH_HOME, temp.getRoot().getAbsolutePath());
try {
- searchServer = new SearchServer(new Props(properties), false, false);
+ searchServer = new SearchServer(new Props(properties));
} catch (Exception e) {
e.printStackTrace();
}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java b/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java
index ce6fad22f77..1a935cb2394 100644
--- a/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java
+++ b/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java
@@ -26,7 +26,6 @@ import org.apache.commons.lang.StringUtils;
import org.junit.rules.ExternalResource;
import org.sonar.api.database.DatabaseProperties;
import org.sonar.api.resources.Language;
-import org.sonar.process.MonitoredProcess;
import org.sonar.process.NetworkUtils;
import org.sonar.process.Props;
import org.sonar.search.SearchServer;
@@ -35,6 +34,7 @@ import org.sonar.server.search.IndexProperties;
import org.sonar.server.ws.WsTester;
import javax.annotation.Nullable;
+
import java.io.File;
import java.util.Arrays;
import java.util.List;
@@ -71,13 +71,8 @@ public class ServerTester extends ExternalResource {
Properties properties = new Properties();
properties.setProperty(IndexProperties.CLUSTER_NAME, clusterName);
properties.setProperty(IndexProperties.NODE_PORT, clusterPort.toString());
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES");
properties.setProperty(SearchServer.SONAR_PATH_HOME, homeDir.getAbsolutePath());
- try {
- searchServer = new SearchServer(new Props(properties), false, false);
- } catch (Exception e) {
- e.printStackTrace();
- }
+ searchServer = new SearchServer(new Props(properties));
}
/**
@@ -99,7 +94,6 @@ public class ServerTester extends ExternalResource {
properties.setProperty(IndexProperties.CLUSTER_NAME, clusterName);
properties.setProperty(IndexProperties.NODE_PORT, clusterPort.toString());
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES");
properties.setProperty("sonar.path.home", homeDir.getAbsolutePath());
properties.setProperty(DatabaseProperties.PROP_URL, "jdbc:h2:" + homeDir.getAbsolutePath() + "/h2");
diff --git a/sonar-application/pom.xml b/sonar-application/pom.xml
index c5f1d9721f3..ee68ed3ce4a 100644
--- a/sonar-application/pom.xml
+++ b/sonar-application/pom.xml
@@ -24,6 +24,11 @@
<artifactId>sonar-process</artifactId>
<version>${project.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.codehaus.sonar</groupId>
+ <artifactId>sonar-process-monitor</artifactId>
+ <version>${project.version}</version>
+ </dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
diff --git a/sonar-application/src/main/java/org/sonar/application/App.java b/sonar-application/src/main/java/org/sonar/application/App.java
index 76ac6a16605..42c4bba84ce 100644
--- a/sonar-application/src/main/java/org/sonar/application/App.java
+++ b/sonar-application/src/main/java/org/sonar/application/App.java
@@ -21,19 +21,18 @@ package org.sonar.application;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import org.sonar.process.JmxUtils;
import org.sonar.process.MinimumViableSystem;
-import org.sonar.process.Monitor;
import org.sonar.process.ProcessLogging;
import org.sonar.process.ProcessMXBean;
-import org.sonar.process.ProcessUtils;
-import org.sonar.process.ProcessWrapper;
import org.sonar.process.Props;
-import org.sonar.search.SearchServer;
+import org.sonar.process.State;
+import org.sonar.process.monitor.JavaCommand;
+import org.sonar.process.monitor.Monitor;
import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Properties;
/**
@@ -41,135 +40,88 @@ import java.util.Properties;
*/
public class App implements ProcessMXBean {
- private Monitor monitor = new Monitor();
- private ProcessWrapper elasticsearch;
- private ProcessWrapper server;
- private boolean success = false;
+ private final Monitor monitor;
public App() {
- JmxUtils.registerMBean(this, "SonarQube");
- ProcessUtils.addSelfShutdownHook(this);
+ this(Monitor.create());
}
- public void start(Props props) throws InterruptedException {
- try {
- Logger logger = LoggerFactory.getLogger(getClass());
+ App(Monitor monitor) {
+ this.monitor = monitor;
+ JmxUtils.registerMBean(this, "SonarQube");
+ }
- monitor.start();
+ public void start(Props props) {
+ monitor.start(createCommands(props));
+ monitor.awaitTermination();
+ }
- File homeDir = props.nonNullValueAsFile("sonar.path.home");
- File tempDir = props.nonNullValueAsFile("sonar.path.temp");
- elasticsearch = new ProcessWrapper(JmxUtils.SEARCH_SERVER_NAME);
- elasticsearch
+ private List<JavaCommand> createCommands(Props props) {
+ List<JavaCommand> commands = new ArrayList<JavaCommand>();
+ File homeDir = props.nonNullValueAsFile("sonar.path.home");
+ File tempDir = props.nonNullValueAsFile("sonar.path.temp");
+ JavaCommand elasticsearch = new JavaCommand(JmxUtils.SEARCH_SERVER_NAME);
+ elasticsearch
+ .setWorkDir(homeDir)
+ .setJmxPort(props.valueAsInt(DefaultSettings.SEARCH_JMX_PORT))
+ .addJavaOptions(props.value(DefaultSettings.SEARCH_JAVA_OPTS))
+ .setTempDir(tempDir.getAbsoluteFile())
+ .setClassName("org.sonar.search.SearchServer")
+ .setArguments(props.rawProperties())
+ .addClasspath("./lib/common/*")
+ .addClasspath("./lib/search/*");
+ commands.add(elasticsearch);
+
+ // do not yet start SQ in cluster mode. See SONAR-5483 & SONAR-5391
+ if (StringUtils.isEmpty(props.value(DefaultSettings.CLUSTER_MASTER))) {
+ JavaCommand webServer = new JavaCommand(JmxUtils.WEB_SERVER_NAME)
.setWorkDir(homeDir)
- .setJmxPort(props.valueAsInt(DefaultSettings.SEARCH_JMX_PORT))
- .addJavaOpts(props.value(DefaultSettings.SEARCH_JAVA_OPTS))
- .setTempDirectory(tempDir.getAbsoluteFile())
- .setClassName("org.sonar.search.SearchServer")
- .addProperties(props.rawProperties())
+ .setJmxPort(props.valueAsInt(DefaultSettings.WEB_JMX_PORT))
+ .addJavaOptions(props.nonNullValue(DefaultSettings.WEB_JAVA_OPTS))
+ .setTempDir(tempDir.getAbsoluteFile())
+ // required for logback tomcat valve
+ .setEnvVariable("sonar.path.logs", props.nonNullValue("sonar.path.logs"))
+ .setClassName("org.sonar.server.app.WebServer")
+ .setArguments(props.rawProperties())
.addClasspath("./lib/common/*")
- .addClasspath("./lib/search/*");
- if (elasticsearch.execute()) {
- monitor.monitor(elasticsearch);
- if (elasticsearch.waitForReady()) {
- logger.info("search server is up");
-
- // do not yet start SQ in cluster mode. See SONAR-5483 & SONAR-5391
- if (StringUtils.isEmpty(props.value(DefaultSettings.CLUSTER_MASTER))) {
- server = new ProcessWrapper(JmxUtils.WEB_SERVER_NAME)
- .setWorkDir(homeDir)
- .setJmxPort(props.valueAsInt(DefaultSettings.WEB_JMX_PORT))
- .addJavaOpts(props.nonNullValue(DefaultSettings.WEB_JAVA_OPTS))
- .setTempDirectory(tempDir.getAbsoluteFile())
- // required for logback tomcat valve
- .setLogDir(props.nonNullValueAsFile("sonar.path.logs"))
- .setClassName("org.sonar.server.app.WebServer")
- .addProperties(props.rawProperties())
- .addClasspath("./lib/common/*")
- .addClasspath("./lib/server/*");
- String driverPath = props.value(JdbcSettings.PROPERTY_DRIVER_PATH);
- if (driverPath != null) {
- server.addClasspath(driverPath);
- }
- if (server.execute()) {
- monitor.monitor(server);
- if (server.waitForReady()) {
- success = true;
- logger.info("web server is up");
- }
- }
- } else {
- success = true;
- }
- }
+ .addClasspath("./lib/server/*");
+ String driverPath = props.value(JdbcSettings.PROPERTY_DRIVER_PATH);
+ if (driverPath != null) {
+ webServer.addClasspath(driverPath);
}
- } finally {
- monitor.join();
- terminate();
+ commands.add(webServer);
}
+ return commands;
}
- static String starPath(File homeDir, String relativePath) {
- File dir = new File(homeDir, relativePath);
- return FilenameUtils.concat(dir.getAbsolutePath(), "*");
+ @Override
+ public void terminate() {
+ monitor.stop();
}
@Override
public boolean isReady() {
- return monitor.isAlive();
+ return monitor.getState() == State.STARTED;
}
@Override
- public long ping() {
- return System.currentTimeMillis();
- }
+ public void ping() {
- @Override
- public void terminate() {
- if (monitor != null && monitor.isAlive()) {
- monitor.terminate();
- monitor = null;
- }
- if (server != null) {
- server.terminate();
- server = null;
- }
- if (elasticsearch != null) {
- elasticsearch.terminate();
- elasticsearch = null;
- }
}
- private boolean isSuccess() {
- return success;
+ static String starPath(File homeDir, String relativePath) {
+ File dir = new File(homeDir, relativePath);
+ return FilenameUtils.concat(dir.getAbsolutePath(), "*");
}
- public static void main(String[] args) {
+ public static void main(String[] args) throws Exception {
new MinimumViableSystem().check();
CommandLineParser cli = new CommandLineParser();
Properties rawProperties = cli.parseArguments(args);
- Props props;
-
- try {
- props = new PropsBuilder(rawProperties, new JdbcSettings()).build();
- new ProcessLogging().configure(props, "/org/sonar/application/logback.xml");
- } catch (Exception e) {
- throw new IllegalStateException(e);
- }
+ Props props = new PropsBuilder(rawProperties, new JdbcSettings()).build();
+ new ProcessLogging().configure(props, "/org/sonar/application/logback.xml");
App app = new App();
- ProcessUtils.addSelfShutdownHook(app);
- try {
- // start and wait for shutdown command
- if (props.contains(SearchServer.ES_CLUSTER_INET)) {
- LoggerFactory.getLogger(App.class).info("SonarQube slave configured to join SonarQube master : {}", props.value(SearchServer.ES_CLUSTER_INET));
- }
- app.start(props);
- } catch (InterruptedException e) {
- LoggerFactory.getLogger(App.class).info("interrupted");
- } finally {
- LoggerFactory.getLogger(App.class).info("stopped");
- System.exit(app.isSuccess() ? 0 : 1);
- }
+ app.start(props);
}
}