summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSimon Brandhof <simon.brandhof@sonarsource.com>2014-09-12 15:56:49 +0200
committerSimon Brandhof <simon.brandhof@sonarsource.com>2014-09-12 15:56:49 +0200
commite5aea553f53345a490c3d7a0dd89e80358f19e5c (patch)
treeb001f21abcb475377a533b1f7e16d2bfa9675dfc
parent0ccbeceffbac2071eccca12f807dde9b2934e3eb (diff)
parent3166381a9020db85e45bfef8ebea9f841736af69 (diff)
downloadsonarqube-e5aea553f53345a490c3d7a0dd89e80358f19e5c.tar.gz
sonarqube-e5aea553f53345a490c3d7a0dd89e80358f19e5c.zip
Merge remote-tracking branch 'origin/branch-4.5'
Conflicts: server/process/pom.xml server/process/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java server/sonar-process-monitor/pom.xml server/sonar-process/pom.xml
-rw-r--r--pom.xml11
-rw-r--r--server/pom.xml3
-rw-r--r--server/process/pom.xml21
-rw-r--r--server/process/sonar-dummy-app/src/main/java/org/sonar/application/DummyOkProcess.java83
-rw-r--r--server/process/sonar-dummy-app/src/main/resources/org/sonar/application/logback.xml24
-rw-r--r--server/process/sonar-process/src/main/java/org/sonar/process/Monitor.java149
-rw-r--r--server/process/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java375
-rw-r--r--server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessWrapperTest.java118
-rw-r--r--server/process/sonar-process/src/test/java/org/sonar/process/DummyProcess.java84
-rw-r--r--server/process/sonar-process/src/test/java/org/sonar/process/MonitorTestBase.java78
-rw-r--r--server/process/sonar-process/src/test/java/org/sonar/process/MonitoredProcessTest.java185
-rw-r--r--server/sonar-process-monitor/pom.xml (renamed from server/process/sonar-dummy-app/pom.xml)91
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java181
-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.java44
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java211
-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.java147
-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.java63
-rw-r--r--server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java118
-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.java34
-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/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.java427
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RmiJmxConnectorTest.java56
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java27
-rw-r--r--server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java56
-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.txt (renamed from server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt)0
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt (renamed from server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt)0
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt (renamed from server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt)0
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt (renamed from server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt)0
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml (renamed from server/process/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml)0
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties (renamed from server/process/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties)0
-rw-r--r--server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties (renamed from server/process/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties)0
-rw-r--r--server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar (renamed from server/process/sonar-process/src/test/resources/sonar-dummy-app.jar)bin854048 -> 854048 bytes
-rw-r--r--server/sonar-process/pom.xml (renamed from server/process/sonar-process/pom.xml)48
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/AesCipher.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/AesCipher.java)0
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java)0
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/Cipher.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/Cipher.java)0
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java)2
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/Encryption.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/Encryption.java)0
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/JmxUtils.java)7
-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.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java)4
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/MessageException.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/MessageException.java)0
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java)0
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java41
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java)3
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java155
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java)0
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java)3
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java)59
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/Props.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/Props.java)0
-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.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/Terminable.java)4
-rw-r--r--server/sonar-process/src/main/java/org/sonar/process/package-info.java (renamed from server/process/sonar-process/src/main/java/org/sonar/process/package-info.java)0
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java (renamed from server/process/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java)0
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java (renamed from server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java)0
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java (renamed from server/process/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java)0
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java (renamed from server/process/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java)0
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java (renamed from server/process/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java)19
-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.java (renamed from server/process/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java)0
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java (renamed from server/process/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java)0
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java (renamed from server/process/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java)0
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java234
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java (renamed from server/process/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java)4
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/PropsTest.java (renamed from server/process/sonar-process/src/test/java/org/sonar/process/PropsTest.java)12
-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.java124
-rw-r--r--server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java86
-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/pom.xml4
-rw-r--r--server/sonar-search/src/main/java/org/sonar/search/SearchServer.java90
-rw-r--r--server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java6
-rw-r--r--server/sonar-server/pom.xml3
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java100
-rw-r--r--server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java34
-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--server/sonar-web/pom.xml3
-rw-r--r--sonar-application/pom.xml12
-rw-r--r--sonar-application/src/main/java/org/sonar/application/App.java166
103 files changed, 3520 insertions, 1545 deletions
diff --git a/pom.xml b/pom.xml
index 21e3b8b432a..1693081db13 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,6 +1,5 @@
<?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/maven-v4_0_0.xsd">
+<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/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.codehaus.sonar</groupId>
@@ -1329,7 +1328,7 @@
</goals>
</pluginExecutionFilter>
<action>
- <ignore/>
+ <ignore />
</action>
</pluginExecution>
<pluginExecution>
@@ -1342,7 +1341,7 @@
</goals>
</pluginExecutionFilter>
<action>
- <ignore/>
+ <ignore />
</action>
</pluginExecution>
<pluginExecution>
@@ -1356,7 +1355,7 @@
</goals>
</pluginExecutionFilter>
<action>
- <ignore/>
+ <ignore />
</action>
</pluginExecution>
<pluginExecution>
@@ -1369,7 +1368,7 @@
</goals>
</pluginExecutionFilter>
<action>
- <ignore/>
+ <ignore />
</action>
</pluginExecution>
</pluginExecutions>
diff --git a/server/pom.xml b/server/pom.xml
index c32d9042576..7a5c9de19d8 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/pom.xml b/server/process/pom.xml
deleted file mode 100644
index bd06cb24b37..00000000000
--- a/server/process/pom.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?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>5.0-SNAPSHOT</version>
- <relativePath>../</relativePath>
- </parent>
- <modelVersion>4.0.0</modelVersion>
-
- <artifactId>process</artifactId>
- <packaging>pom</packaging>
- <name>SonarQube :: Process :: Parent</name>
-
- <modules>
- <module>sonar-dummy-app</module>
- <module>sonar-process</module>
- </modules>
-</project>
diff --git a/server/process/sonar-dummy-app/src/main/java/org/sonar/application/DummyOkProcess.java b/server/process/sonar-dummy-app/src/main/java/org/sonar/application/DummyOkProcess.java
deleted file mode 100644
index b25dfccc236..00000000000
--- a/server/process/sonar-dummy-app/src/main/java/org/sonar/application/DummyOkProcess.java
+++ /dev/null
@@ -1,83 +0,0 @@
-/*
- * 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.application;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.sonar.process.MonitoredProcess;
-import org.sonar.process.Props;
-
-import java.io.File;
-import java.util.Properties;
-
-public class DummyOkProcess extends MonitoredProcess {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(DummyOkProcess.class);
-
- private boolean isReady = false;
- private boolean isRunning = true;
- private boolean isSuccess = true;
-
- protected DummyOkProcess(Props props) {
- super(props);
- try {
- File.createTempFile("hello", ".tmp");
- } catch (Exception e) {
- LOGGER.error("Could not create file", e);
- isSuccess = false;
- }
- }
-
- @Override
- protected void doStart() {
- isReady = true;
- LOGGER.info("Starting Dummy OK Process");
- while (isRunning) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- isRunning = false;
- }
- }
- }
-
- @Override
- protected void doTerminate() {
- LOGGER.info("Terminating Dummy OK Process");
- this.isRunning = false;
- }
-
- @Override
- protected boolean doIsReady() {
- return isReady;
- }
-
- private boolean isSuccess() {
- return isSuccess;
- }
-
- public static int main(String[] args) {
- Props props = new Props(new Properties());
- props.set(MonitoredProcess.NAME_PROPERTY, DummyOkProcess.class.getSimpleName());
- DummyOkProcess process = new DummyOkProcess(props);
- process.start();
- return (process.isSuccess()) ? 1 : 0;
- }
-}
diff --git a/server/process/sonar-dummy-app/src/main/resources/org/sonar/application/logback.xml b/server/process/sonar-dummy-app/src/main/resources/org/sonar/application/logback.xml
deleted file mode 100644
index 933930557b9..00000000000
--- a/server/process/sonar-dummy-app/src/main/resources/org/sonar/application/logback.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-
-<!--
- Configuration for default logger. Only used while embedded server is starting,
- before proper logging configuration is loaded.
-
- See http://logback.qos.ch/manual/configuration.html
--->
-<configuration debug="false">
- <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/>
-
- <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
- <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
- <pattern>
- %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n
- </pattern>
- </encoder>
- </appender>
-
- <root>
- <level value="DEBUG"/>
- <appender-ref ref="CONSOLE"/>
- </root>
-</configuration>
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Monitor.java b/server/process/sonar-process/src/main/java/org/sonar/process/Monitor.java
deleted file mode 100644
index baa0f00791c..00000000000
--- a/server/process/sonar-process/src/main/java/org/sonar/process/Monitor.java
+++ /dev/null
@@ -1,149 +0,0 @@
-/*
- * 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.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-public class Monitor extends Thread implements Terminable {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(Monitor.class);
-
- private static final long PING_DELAY_MS = 3000L;
-
- private long pingDelayMs = PING_DELAY_MS;
- private final List<ProcessWrapper> processes = new CopyOnWriteArrayList<ProcessWrapper>();
- private final ScheduledFuture<?> watch;
- private final ScheduledExecutorService monitorExecutionService;
-
- /**
- * Starts another thread to send ping to all registered processes
- */
- public Monitor() {
- super("Process Monitor");
- monitorExecutionService = Executors.newScheduledThreadPool(1);
- watch = monitorExecutionService.scheduleAtFixedRate(new ProcessWatch(), 0L, getPingDelayMs(), TimeUnit.MILLISECONDS);
- }
-
- private long getPingDelayMs() {
- return pingDelayMs;
- }
-
- public Monitor setPingDelayMs(long pingDelayMs) {
- this.pingDelayMs = pingDelayMs;
- return this;
- }
-
- private class ProcessWatch extends Thread {
- private ProcessWatch() {
- super("Process Ping");
- }
-
- @Override
- public void run() {
- for (ProcessWrapper process : processes) {
- LOGGER.debug("Pinging process[{}]", process.getName());
- try {
- ProcessMXBean mBean = process.getProcessMXBean();
- if (mBean != null) {
- mBean.ping();
- }
- } catch (Exception e) {
- LOGGER.debug("Could not ping process[{}]", process.getName());
- LOGGER.trace("Ping failure", e);
- }
- }
- }
- }
-
- /**
- * Registers and monitors process. Note that process is probably not ready yet.
- */
- public void monitor(ProcessWrapper process) throws InterruptedException {
- LOGGER.info("Monitoring process[{}]", process.getName());
- // starts a monitoring thread
- process.start();
- processes.add(process);
- }
-
- /**
- * Check continuously that registered processes are still up. If any process is down or does not answer to pings
- * during the max allowed period, then thread exits.
- */
- @Override
- public void run() {
- try {
- boolean ok = true;
- while (isRunning && ok) {
- LOGGER.debug("Monitoring {} processes.", processes.size());
- for (ProcessWrapper process : processes) {
- if (!ProcessUtils.isAlive(process.process())) {
- LOGGER.info("{} is down, stopping all other processes", process.getName());
- ok = false;
- interrupt();
- }
- }
- if (ok) {
- Thread.sleep(PING_DELAY_MS);
- }
- }
- } catch (InterruptedException e) {
- LOGGER.debug("Monitoring thread is interrupted");
- } finally {
- terminate();
- }
- }
-
- volatile Boolean isRunning = true;
-
- @Override
- public synchronized void terminate() {
-
- LOGGER.debug("Monitoring thread is terminating");
-
- if (!monitorExecutionService.isShutdown()) {
- monitorExecutionService.shutdownNow();
- }
- if (!watch.isCancelled()) {
- watch.cancel(true);
- }
-
- processes.clear();
- interruptAndWait();
- }
-
- private void interruptAndWait() {
- this.interrupt();
- try {
- if (this.isAlive()) {
- this.join();
- }
- } catch (InterruptedException e) {
- // Expected to be interrupted :)
- }
- }
-}
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java b/server/process/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java
deleted file mode 100644
index 869867a6e34..00000000000
--- a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java
+++ /dev/null
@@ -1,375 +0,0 @@
-/*
- * 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.apache.commons.lang.StringUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.annotation.CheckForNull;
-import javax.annotation.Nullable;
-import javax.management.JMX;
-import javax.management.MBeanServerConnection;
-import javax.management.Notification;
-import javax.management.NotificationListener;
-import javax.management.remote.JMXConnector;
-import javax.management.remote.JMXConnectorFactory;
-import javax.management.remote.JMXServiceURL;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Properties;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Fork and monitor a new process
- */
-public class ProcessWrapper extends Thread implements Terminable {
-
- private static final Logger LOGGER = LoggerFactory.getLogger(ProcessWrapper.class);
-
- public static final long READY_TIMEOUT_MS = 300000L;
-
- private String processName, className;
- private int jmxPort = -1;
- private final List<String> javaOpts = new ArrayList<String>();
- private final List<String> classpath = new ArrayList<String>();
- private final Map<String, String> envProperties = new HashMap<String, String>();
- private final Properties properties = new Properties();
- private File workDir;
- private Process process;
- private StreamGobbler errorGobbler;
- private StreamGobbler outputGobbler;
- private ProcessMXBean processMXBean;
- private final Object terminationLock = new Object();
-
- public ProcessWrapper(String processName) {
- super(processName);
- this.processName = processName;
- }
-
- public ProcessWrapper setClassName(String s) {
- this.className = s;
- return this;
- }
-
- public ProcessWrapper setEnvProperty(String key, String value) {
- envProperties.put(key, value);
- return this;
- }
-
- public ProcessWrapper addProperties(Properties p) {
- properties.putAll(p);
- return this;
- }
-
- public ProcessWrapper addProperty(String key, String value) {
- properties.setProperty(key, value);
- return this;
- }
-
- public ProcessWrapper addJavaOpts(String s) {
- Collections.addAll(javaOpts, s.split(" "));
- return this;
- }
-
- public ProcessWrapper addClasspath(String s) {
- classpath.add(s);
- return this;
- }
-
- public ProcessWrapper setJmxPort(int i) {
- this.jmxPort = i;
- return this;
- }
-
- public ProcessWrapper setWorkDir(File d) {
- this.workDir = d;
- return this;
- }
-
- public ProcessWrapper setTempDirectory(File tempDirectory) {
- this.setEnvProperty("java.io.tmpdir", tempDirectory.getAbsolutePath());
- return this;
- }
-
- public ProcessWrapper setLogDir(File logDirectory) {
- this.setEnvProperty("sonar.path.logs", logDirectory.getAbsolutePath());
- return this;
- }
-
- @CheckForNull
- public Process process() {
- return process;
- }
-
- /**
- * Execute command-line and connects to JMX RMI.
- *
- * @return true on success, false if bad command-line or process failed to start JMX RMI
- */
- public boolean execute() {
- List<String> command = new ArrayList<String>();
- try {
- command.add(buildJavaCommand());
- command.addAll(javaOpts);
- command.addAll(buildJMXOptions());
- command.addAll(buildClasspath());
- command.add(className);
- command.add(buildPropertiesFile().getAbsolutePath());
-
- ProcessBuilder processBuilder = new ProcessBuilder();
- processBuilder.command(command);
- processBuilder.directory(workDir);
- processBuilder.environment().putAll(envProperties);
- LOGGER.info("starting {}: {}", getName(), StringUtils.join(command, " "));
- process = processBuilder.start();
- errorGobbler = new StreamGobbler(process.getErrorStream(), this.getName() + "-ERROR");
- outputGobbler = new StreamGobbler(process.getInputStream(), this.getName());
- outputGobbler.start();
- errorGobbler.start();
- processMXBean = waitForJMX();
- if (processMXBean == null) {
- terminate();
- return false;
- }
- return true;
- } catch (Exception e) {
- throw new IllegalStateException("Fail to start command: " + StringUtils.join(command, " "), e);
- }
- }
-
- @Override
- public void run() {
- try {
- if (ProcessUtils.isAlive(process)) {
- process.waitFor();
- }
- } catch (Exception e) {
- LOGGER.info("ProcessThread has been interrupted. Killing node.");
- LOGGER.trace("Process exception", e);
- }
- }
-
- public boolean isReady() {
- return processMXBean != null && processMXBean.isReady();
- }
-
- public ProcessMXBean getProcessMXBean() {
- return processMXBean;
- }
-
- private String buildJavaCommand() {
- String separator = System.getProperty("file.separator");
- return new File(new File(System.getProperty("java.home")),
- "bin" + separator + "java").getAbsolutePath();
- }
-
- private List<String> buildJMXOptions() {
- if (jmxPort < 1) {
- throw new IllegalStateException("JMX port is not set");
- }
- return Arrays.asList(
- "-Dcom.sun.management.jmxremote",
- "-Dcom.sun.management.jmxremote.port=" + jmxPort,
- "-Dcom.sun.management.jmxremote.authenticate=false",
- "-Dcom.sun.management.jmxremote.ssl=false",
- "-Djava.rmi.server.hostname=" + LoopbackAddress.get().getHostAddress());
- }
-
- private List<String> buildClasspath() {
- return Arrays.asList("-cp", StringUtils.join(classpath, System.getProperty("path.separator")));
- }
-
- private File buildPropertiesFile() {
- File propertiesFile = null;
- try {
- propertiesFile = File.createTempFile("sq-conf", "properties");
- Properties props = new Properties();
- props.putAll(properties);
- props.put(MonitoredProcess.NAME_PROPERTY, processName);
- OutputStream out = new FileOutputStream(propertiesFile);
- props.store(out, "Temporary properties file for Process [" + getName() + "]");
- out.close();
- return propertiesFile;
- } catch (IOException e) {
- throw new IllegalStateException("Cannot write temporary settings to " + propertiesFile, e);
- }
- }
-
- /**
- * Wait for JMX RMI to be ready. Return <code>null</code>
- */
- @CheckForNull
- private ProcessMXBean waitForJMX() {
- JMXServiceURL jmxUrl = JmxUtils.serviceUrl(LoopbackAddress.get(), jmxPort);
- for (int i = 0; i < 5; i++) {
- try {
- Thread.sleep(1000L);
- JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, null);
- jmxConnector.addConnectionNotificationListener(new NotificationListener() {
- @Override
- public void handleNotification(Notification notification, Object handback) {
- LOGGER.debug("JMX Connection Notification:{}", notification.getMessage());
- }
- }, null, null);
- MBeanServerConnection mBeanServer = jmxConnector.getMBeanServerConnection();
- return JMX.newMBeanProxy(mBeanServer, JmxUtils.objectName(processName), ProcessMXBean.class);
- } catch (Exception ignored) {
- LOGGER.info(String.format("Could not connect to JMX (attempt %d). Trying again.", i), ignored);
- }
- }
- // failed to connect
- return null;
- }
-
- @Override
- public void terminate() {
- synchronized (terminationLock) {
- if (processMXBean != null && process != null) {
- LOGGER.info("{} stopping", getName());
- // Send the terminate command to process in order to gracefully shutdown.
- // Then hardly kill it if it didn't terminate in 30 seconds
- ScheduledExecutorService killer = Executors.newScheduledThreadPool(1);
- try {
- Runnable killerTask = new Runnable() {
- @Override
- public void run() {
- ProcessUtils.destroyQuietly(process);
- }
- };
-
- ScheduledFuture killerFuture = killer.schedule(killerTask, 30, TimeUnit.SECONDS);
- processMXBean.terminate();
- this.join();
- killerFuture.cancel(true);
- LOGGER.info("{} stopped", getName());
-
- } catch (Exception ignored) {
- LOGGER.trace("Could not terminate process", ignored);
- } finally {
- killer.shutdownNow();
- interruptAndWait();
- }
- } else {
- // process is not monitored through JMX, but killing it though
- ProcessUtils.destroyQuietly(process);
- }
- processMXBean = null;
- }
- }
-
- public boolean waitForReady() throws InterruptedException {
- long now = 0;
- long wait = 500L;
- while (now < READY_TIMEOUT_MS) {
- try {
- if (processMXBean == null) {
- return false;
- }
- if (processMXBean.isReady()) {
- return true;
- }
- } catch (Exception e) {
- LOGGER.trace("Process is not ready yet", e);
- }
- Thread.sleep(wait);
- now += wait;
- }
- return false;
- }
-
- private void interruptAndWait() {
- try {
- //after being interrupted, finalize the goblins
- if (outputGobbler != null && outputGobbler.isAlive()) {
- waitUntilFinish(outputGobbler);
- }
-
- if (errorGobbler != null && errorGobbler.isAlive()) {
- waitUntilFinish(errorGobbler);
- }
- if (process != null) {
- ProcessUtils.closeStreams(process);
- }
-
- //Join while the main thread terminates
- if (this.isAlive()) {
- this.join();
- }
- } catch (InterruptedException e) {
- // Expected to be interrupted :)
- }
- }
-
- private void waitUntilFinish(@Nullable Thread thread) {
- if (thread != null && thread.isAlive()) {
- try {
- thread.join();
- } catch (InterruptedException e) {
- LOGGER.error("InterruptedException while waiting finish of " + thread.getName() + " in process '" + getName() + "'", e);
- }
- }
- }
-
-
- private static class StreamGobbler extends Thread {
- private final InputStream is;
- private final Logger logger;
-
- StreamGobbler(InputStream is, String name) {
- super(name + "_ProcessStreamGobbler");
- this.is = is;
- this.logger = LoggerFactory.getLogger(name);
- }
-
- @Override
- public void run() {
- InputStreamReader isr = new InputStreamReader(is);
- BufferedReader br = new BufferedReader(isr);
- try {
- String line;
- while ((line = br.readLine()) != null) {
- logger.info(line);
- }
- } catch (Exception ignored) {
- logger.trace("Error while Gobbling", ignored);
- } finally {
- IOUtils.closeQuietly(br);
- IOUtils.closeQuietly(isr);
- }
- }
- }
-}
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessWrapperTest.java b/server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessWrapperTest.java
deleted file mode 100644
index b217edb2584..00000000000
--- a/server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessWrapperTest.java
+++ /dev/null
@@ -1,118 +0,0 @@
-/*
- * 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.FilenameUtils;
-import org.junit.Test;
-
-import java.io.File;
-import java.util.Properties;
-
-import static org.fest.assertions.Assertions.assertThat;
-import static org.fest.assertions.Fail.fail;
-
-
-public class BaseProcessWrapperTest extends BaseProcessTest {
-
- @Test
- public void has_dummy_app() {
- assertThat(dummyAppJar).isFile();
- assertThat(dummyAppJar).exists();
- }
-
- private void assertCanStart(ProcessWrapper process) {
- assertThat(process.execute()).isTrue();
- proc = process.process();
- }
-
- private void assertCanBeReady(ProcessWrapper process) throws InterruptedException {
- int count = 0;
- while (!process.isReady() && count < 5) {
- Thread.sleep(500);
- }
- assertThat(process.getProcessMXBean().isReady()).isTrue();
- }
-
- private void assertPing(ProcessWrapper process) {
- long now = System.currentTimeMillis();
- long ping = process.getProcessMXBean().ping();
- assertThat(ping - now).isLessThan(3000L);
- }
-
-
- @Test
- public void execute_dummy_app() throws Exception {
-
- ProcessWrapper process = new ProcessWrapper("DummyOkProcess")
- .addProperty(MonitoredProcess.NAME_PROPERTY, "DummyOkProcess")
- .addClasspath(dummyAppJar.getAbsolutePath())
- .setWorkDir(temp.getRoot())
- .setTempDirectory(temp.getRoot())
- .setJmxPort(freePort)
- .setClassName(DUMMY_OK_APP);
-
- assertThat(process.isAlive()).isFalse();
- assertCanStart(process);
- process.start();
- assertCanBeReady(process);
- assertThat(process.isAlive()).isTrue();
- assertPing(process);
- process.terminate();
- try {
- assertPing(process);
- fail();
- } catch (Exception e) {
-
- }
- }
-
-
- @Test
- public void execute_dummy_in_space_folder_app() throws Exception {
-
- // 0 create a home with space...
- File home = temp.newFolder("t est");
- assertThat(home.canWrite()).isTrue();
- File lib = new File(home, "lib");
- File tempdir = new File(home, "temp");
- FileUtils.copyFileToDirectory(dummyAppJar, lib);
-
- // 1 Create Properties
- Props props = new Props(new Properties());
- props.set("spaceHome", home.getAbsolutePath());
-
- // 3 start dummy app
- File effectiveHome = props.nonNullValueAsFile("spaceHome");
-
- String cp = FilenameUtils.concat(new File(effectiveHome, "lib").getAbsolutePath(), "*");
- System.out.println("cp = " + cp);
- ProcessWrapper process = new ProcessWrapper("DummyOkProcess")
- .addProperty(MonitoredProcess.NAME_PROPERTY, "DummyOkProcess")
- .setTempDirectory(tempdir)
- .addClasspath(cp)
- .setWorkDir(home)
- .setJmxPort(freePort)
- .setClassName(DUMMY_OK_APP);
-
- assertThat(process.isAlive()).isFalse();
- assertCanStart(process);
- }
-}
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/DummyProcess.java b/server/process/sonar-process/src/test/java/org/sonar/process/DummyProcess.java
deleted file mode 100644
index f16c61fd93c..00000000000
--- a/server/process/sonar-process/src/test/java/org/sonar/process/DummyProcess.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * 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.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.util.Properties;
-
-public class DummyProcess extends MonitoredProcess {
-
- public static final String NAME = "DummyName";
- public static final String CHECKFILE_NAME = "check.tmp";
-
- private static final Logger LOGGER = LoggerFactory.getLogger(DummyProcess.class);
-
- private boolean isReady = false;
- private boolean isRunning = true;
- private File checkFile;
-
-
- protected DummyProcess(Props props, boolean monitored) throws Exception {
- super(props, monitored);
- }
-
- protected DummyProcess(Props props) throws Exception {
- super(props);
- }
-
- @Override
- protected void doStart() {
- isReady = true;
- checkFile = new File(FileUtils.getTempDirectory(), CHECKFILE_NAME);
- LOGGER.info("Starting Dummy OK Process");
- while (isRunning) {
- try {
- Thread.sleep(100);
- } catch (InterruptedException e) {
- isRunning = false;
- }
- }
- }
-
- @Override
- protected void doTerminate() {
- LOGGER.info("Terminating Dummy OK Process");
- this.isRunning = false;
- }
-
- @Override
- protected boolean doIsReady() {
- return isReady;
- }
-
- public static void main(String[] args) throws Exception {
- Props props = new Props(new Properties());
- props.set(MonitoredProcess.NAME_PROPERTY, DummyProcess.class.getSimpleName());
- new DummyProcess(props).start();
- System.exit(1);
- }
-
- public File getCheckFile() {
- return checkFile;
- }
-}
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/MonitorTestBase.java b/server/process/sonar-process/src/test/java/org/sonar/process/MonitorTestBase.java
deleted file mode 100644
index fb9c818d017..00000000000
--- a/server/process/sonar-process/src/test/java/org/sonar/process/MonitorTestBase.java
+++ /dev/null
@@ -1,78 +0,0 @@
-/*
- * 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.After;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.fest.assertions.Assertions.assertThat;
-
-public class MonitorTestBase extends BaseProcessTest {
-
-
- Monitor monitor;
-
- @Before
- public void setUpMonitor() throws Exception {
- monitor = new Monitor();
- }
-
- @After
- public void downMonitor() throws Exception {
- if (monitor != null) {
- monitor.interrupt();
- monitor = null;
- }
- }
-
- @Test
- public void monitor_can_start_and_stop() {
- assertThat(monitor.isAlive()).isFalse();
- monitor.start();
- assertThat(monitor.isAlive()).isTrue();
- monitor.terminate();
- assertThat(monitor.isAlive()).isFalse();
- }
-
- @Test(timeout = 2500L)
- public void monitor_should_interrupt_process() throws Exception {
- // 0 start the dummyProcess
- ProcessWrapper process = new ProcessWrapper("DummyOkProcess")
- .addProperty(MonitoredProcess.NAME_PROPERTY, "DummyOkProcess")
- .addClasspath(dummyAppJar.getAbsolutePath())
- .setWorkDir(temp.getRoot())
- .setTempDirectory(temp.getRoot())
- .setJmxPort(freePort)
- .setClassName(DUMMY_OK_APP);
-
- assertThat(process.execute());
-
-
- // 1 start my monitor & register process
- monitor.start();
- monitor.monitor(process);
-
- // 2 terminate monitor, assert process is terminated
- monitor.terminate();
- assertThat(monitor.isAlive()).isFalse();
- assertThat(process.isAlive()).isFalse();
- }
-}
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/MonitoredProcessTest.java b/server/process/sonar-process/src/test/java/org/sonar/process/MonitoredProcessTest.java
deleted file mode 100644
index c753bd00422..00000000000
--- a/server/process/sonar-process/src/test/java/org/sonar/process/MonitoredProcessTest.java
+++ /dev/null
@@ -1,185 +0,0 @@
-/*
- * 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 MonitoredProcessTest {
-
- @Test
- public void fail_on_missing_name() throws Exception {
- Properties properties = new Properties();
-
- try {
- new DummyProcess(new Props(properties), true);
- fail();
- } catch (IllegalArgumentException e) {
- assertThat(e.getMessage()).isEqualTo("Missing property: pName");
- }
-
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME);
- DummyProcess dummyProcess = new DummyProcess(new Props(properties), true);
- assertThat(dummyProcess).isNotNull();
- }
-
- @Test
- public void should_not_monitor_debug() throws Exception {
- Properties properties = new Properties();
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME);
- DummyProcess dummyProcess = new DummyProcess(new Props(properties), false);
-
- assertThat(dummyProcess.isMonitored()).isFalse();
- }
-
- @Test
- public void should_monitor_by_default() throws Exception {
- Properties properties = new Properties();
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME);
- properties.setProperty("sonar.search.javaOpts", "hello world");
- DummyProcess dummyProcess = new DummyProcess(new Props(properties));
-
- assertThat(dummyProcess.isMonitored()).isTrue();
- }
-
- @Test(timeout = 3000L)
- public void monitor_dies_when_no_pings() throws Exception {
- Properties properties = new Properties();
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME);
- final DummyProcess dummyProcess = new DummyProcess(new Props(properties), true);
- dummyProcess.setTimeout(1000L)
- .setCheckDelay(500L);
- Thread process = new Thread(new Runnable() {
- @Override
- public void run() {
- dummyProcess.start();
- }
- });
- assertProcessNotYetRunning(dummyProcess);
- process.start();
- Thread.sleep(100);
-
- assertProcessRunning(dummyProcess);
- assertJoinAndTerminate(dummyProcess, process);
- }
-
- @Test(timeout = 3000L)
- public void monitor_dies_after_stopping_to_ping() throws Exception {
- Properties properties = new Properties();
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME);
- final DummyProcess dummyProcess = new DummyProcess(new Props(properties), true);
- dummyProcess.setTimeout(1000L)
- .setCheckDelay(500L);
- Thread process = new Thread(new Runnable() {
- @Override
- public void run() {
- dummyProcess.start();
- }
- });
- assertProcessNotYetRunning(dummyProcess);
- process.start();
- Thread.sleep(100);
-
- int count = 0;
- for (int i = 0; i < 3; i++) {
- dummyProcess.ping();
- assertProcessRunning(dummyProcess);
- Thread.sleep(300);
- count++;
- }
- assertThat(count).isEqualTo(3);
- assertJoinAndTerminate(dummyProcess, process);
- }
-
- @Test(timeout = 3000L)
- public void monitor_explicitly_shutdown() throws Exception {
- Properties properties = new Properties();
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME);
- final DummyProcess dummyProcess = new DummyProcess(new Props(properties), true);
- dummyProcess.setTimeout(Long.MAX_VALUE).setCheckDelay(500L);
- Thread process = new Thread(new Runnable() {
- @Override
- public void run() {
- dummyProcess.start();
- }
- });
- assertProcessNotYetRunning(dummyProcess);
- process.start();
- Thread.sleep(100);
- assertProcessRunning(dummyProcess);
- dummyProcess.terminate();
- Thread.sleep(100);
- assertProcessTerminated(dummyProcess);
- }
-
- @Test(timeout = 1000L)
- public void process_does_not_die_when_debugged() throws Exception {
- Properties properties = new Properties();
- properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME);
-
- final DummyProcess dummyProcess = new DummyProcess(new Props(properties), false);
- assertThat(dummyProcess.isMonitored()).isFalse();
-
- dummyProcess.setTimeout(100L).setCheckDelay(100L);
- Thread process = new Thread(new Runnable() {
- @Override
- public void run() {
- dummyProcess.start();
- }
- });
- process.start();
- Thread.sleep(600);
-
- assertProcessRunning(dummyProcess);
- dummyProcess.terminate();
- assertProcessTerminated(dummyProcess);
- }
-
- private void assertJoinAndTerminate(DummyProcess dummyProcess, Thread process) throws InterruptedException {
- process.join();
- assertProcessTerminated(dummyProcess);
- }
-
- private void assertProcessTerminated(DummyProcess dummyProcess) {
- assertThat(dummyProcess.isReady()).isTrue();
- assertThat(dummyProcess.isTerminated()).isTrue();
- assertProcessCreatedFile(dummyProcess);
- }
-
- private void assertProcessNotYetRunning(DummyProcess dummyProcess) {
- assertThat(dummyProcess.isReady()).isFalse();
- assertThat(dummyProcess.isTerminated()).isFalse();
- }
-
- private void assertProcessRunning(DummyProcess dummyProcess) throws InterruptedException {
- assertThat(dummyProcess.isReady()).isTrue();
- assertThat(dummyProcess.isTerminated()).isFalse();
- }
-
- private void assertProcessCreatedFile(DummyProcess dummyProcess) {
- assertThat(dummyProcess.getCheckFile()).isNotNull();
- assertThat(dummyProcess.getCheckFile().getName()).isEqualTo(DummyProcess.CHECKFILE_NAME);
- }
-
-}
diff --git a/server/process/sonar-dummy-app/pom.xml b/server/sonar-process-monitor/pom.xml
index 9c0f067ebbb..fa0869a52b8 100644
--- a/server/process/sonar-dummy-app/pom.xml
+++ b/server/sonar-process-monitor/pom.xml
@@ -4,16 +4,14 @@
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>process</artifactId>
- <version>5.0-SNAPSHOT</version>
+ <artifactId>server</artifactId>
+ <version>4.5-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
- <artifactId>sonar-dummy-app</artifactId>
- <packaging>jar</packaging>
- <name>SonarQube :: Process :: DummyApp</name>
- <description>Dummy Application to test sonar-process</description>
+ <artifactId>sonar-process-monitor</artifactId>
+ <name>SonarQube :: Process Monitor</name>
<dependencies>
<dependency>
@@ -22,23 +20,32 @@
<version>${project.version}</version>
</dependency>
<dependency>
- <groupId>com.google.code.findbugs</groupId>
- <artifactId>jsr305</artifactId>
- <scope>provided</scope>
- </dependency>
-
-
- <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
- <scope>runtime</scope>
</dependency>
- <!-- testing -->
+ <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>
@@ -59,42 +66,22 @@
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-jar-plugin</artifactId>
- <configuration>
- <archive>
- <manifest>
- <addClasspath>false</addClasspath>
- <mainClass>org.sonar.application.DummyOkProcess</mainClass>
- </manifest>
- </archive>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-shade-plugin</artifactId>
- <version>2.3</version>
- <executions>
- <execution>
- <phase>package</phase>
- <goals>
- <goal>shade</goal>
- </goals>
- <configuration>
- <keepDependenciesWithProvidedScope>false</keepDependenciesWithProvidedScope>
- <minimizeJar>true</minimizeJar>
- </configuration>
- </execution>
- </executions>
- </plugin>
- </plugins>
- </build>
+ <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>
- <properties>
- <sonar.exclusions>**/*.java</sonar.exclusions>
- </properties>
+ </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..e950c92c58c
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java
@@ -0,0 +1,181 @@
+/*
+ * 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;
+ }
+
+ /**
+ * Shortcut to set the java option -Djava.io.tmpdir
+ */
+ public JavaCommand setTempDir(File tempDir) {
+ this.javaOptions.add("-Djava.io.tmpdir=" + tempDir.getAbsolutePath());
+ return this;
+ }
+
+ public int getJmxPort() {
+ return jmxPort;
+ }
+
+ /**
+ * Current mandatory to be set
+ */
+ 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..c996595ba73
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java
@@ -0,0 +1,44 @@
+/*
+ * 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 {
+
+ /**
+ * Throws an exception if timeout reached
+ */
+ void connect(JavaCommand command, ProcessRef processRef, long timeoutMs);
+
+ void ping(ProcessRef process);
+
+ /**
+ * Throws an exception if timeout reached
+ */
+ boolean isReady(ProcessRef process, long timeoutMs);
+
+ /**
+ * Throws an exception if timeout reached
+ */
+ void terminate(ProcessRef process, long timeoutMs);
+
+}
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..518d2c354cd
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java
@@ -0,0 +1,211 @@
+/*
+ * 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, 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, timeouts.getJmxConnectionTimeout());
+ 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, timeouts.getMonitorIsReadyTimeout());
+ } catch (Exception ignored) {
+ // failed to send request, probably because RMI server is still not alive
+ // trying again, as long as process is alive
+ // TODO could be improved to have a STARTING timeout (to be implemented in monitor or
+ // in child process ?)
+ }
+ 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..876ccc2b22f
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java
@@ -0,0 +1,147 @@
+/*
+ * 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.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>();
+
+ @Override
+ public synchronized void connect(final JavaCommand command, ProcessRef processRef, long timeoutMs) {
+ ConnectorCallable callable = new ConnectorCallable(command, processRef.getProcess());
+ ProcessMXBean mxBean = execute(callable, timeoutMs);
+ if (mxBean != null) {
+ register(processRef, mxBean);
+ } else if (!processRef.isTerminated()) {
+ throw new IllegalStateException("Fail to connect to JMX RMI server of " + processRef);
+ }
+ }
+
+ @Override
+ public void ping(ProcessRef processRef) {
+ mbeans.get(processRef).ping();
+ }
+
+ @Override
+ public boolean isReady(final ProcessRef processRef, long timeoutMs) {
+ return execute(new Callable<Boolean>() {
+ @Override
+ public Boolean call() throws Exception {
+ return mbeans.get(processRef).isReady();
+ }
+ }, timeoutMs);
+ }
+
+ @Override
+ public void terminate(final ProcessRef processRef, long timeoutMs) {
+ execute(new Callable() {
+ @Override
+ public Void call() throws Exception {
+ LoggerFactory.getLogger(getClass()).info("Request termination of " + processRef);
+ mbeans.get(processRef).terminate();
+ return null;
+ }
+ }, timeoutMs);
+ }
+
+ void register(ProcessRef processRef, ProcessMXBean mxBean) {
+ mbeans.put(processRef, mxBean);
+ }
+
+ private <T> T execute(Callable<T> callable, long timeoutMs) {
+ ExecutorService executor = Executors.newSingleThreadExecutor();
+ try {
+ Future<T> future = executor.submit(callable);
+ return future.get(timeoutMs, TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail send JMX request", e);
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
+ private static class ConnectorCallable implements Callable<ProcessMXBean> {
+ private final JavaCommand command;
+ private final Process process;
+
+ 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 ignored) {
+ // ignored, RMI server is probably not started yet
+ }
+ 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..f5b11b3148f
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java
@@ -0,0 +1,63 @@
+/*
+ * 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;
+
+/**
+ * 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);
+ try {
+ jmxConnector.terminate(processRef, timeouts.getTerminationTimeout());
+ } catch (Exception ignored) {
+ // failed to gracefully stop in a timely fashion
+ LoggerFactory.getLogger(getClass()).info(String.format("Kill %s", processRef));
+ } finally {
+ // 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..68934f21066
--- /dev/null
+++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java
@@ -0,0 +1,118 @@
+/*
+ * 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 monitorIsReadyTimeout = 10000L;
+ 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;
+ }
+
+ /**
+ * [monitor] Timeout of isReady request
+ */
+ long getMonitorIsReadyTimeout() {
+ return monitorIsReadyTimeout;
+ }
+
+ /**
+ * @see #getMonitorIsReadyTimeout()
+ */
+ void setMonitorIsReadyTimeout(long l) {
+ this.monitorIsReadyTimeout = 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..2010e19aa87
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java
@@ -0,0 +1,34 @@
+/*
+ * 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;
+
+ @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..055e3c8e50b
--- /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, long timeoutMs) {
+ throw new IllegalStateException("Test - Impossible to connect to JMX");
+ }
+
+ @Override
+ public void ping(ProcessRef process) {
+
+ }
+
+ @Override
+ public boolean isReady(ProcessRef process, long timeoutMs) {
+ return false;
+ }
+
+ @Override
+ public void terminate(ProcessRef process, long timeoutMs) {
+
+ }
+}
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..766f24c86fc
--- /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("JAVA_COMMAND_TEST", "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");
+
+ // copy current env variables
+ assertThat(command.getEnvVariables().get("JAVA_COMMAND_TEST")).isEqualTo("1000");
+ assertThat(command.getEnvVariables().size()).isEqualTo(System.getenv().size() + 1);
+ }
+
+ @Test
+ public void 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..6f0be0eb5be
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java
@@ -0,0 +1,427 @@
+/*
+ * 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_fail_to_request_gracefully_termination() throws Exception {
+ Timeouts timeouts = new Timeouts();
+ timeouts.setTerminationTimeout(100L);
+ monitor = new Monitor(new JavaProcessLauncher(timeouts),
+ new TerminationFailureRmiConnector(), 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();
+ 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, 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/RmiJmxConnectorTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RmiJmxConnectorTest.java
new file mode 100644
index 00000000000..c4a0f735d60
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RmiJmxConnectorTest.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.monitor;
+
+import org.junit.Test;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+import org.sonar.process.ProcessMXBean;
+
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class RmiJmxConnectorTest {
+
+ @Test
+ public void throw_exception_on_timeout() throws Exception {
+ RmiJmxConnector connector = new RmiJmxConnector();
+ ProcessRef ref = mock(ProcessRef.class);
+ ProcessMXBean mxBean = mock(ProcessMXBean.class);
+ connector.register(ref, mxBean);
+
+ when(mxBean.isReady()).thenAnswer(new Answer<Boolean>() {
+ @Override
+ public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable {
+ Thread.sleep(Long.MAX_VALUE);
+ return null;
+ }
+ });
+
+ try {
+ connector.isReady(ref, 5L);
+ fail();
+ } catch (IllegalStateException e) {
+ assertThat(e).hasMessage("Fail send JMX request");
+ }
+ }
+}
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..0f9f5702666
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.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.monitor;
+
+public class TerminationFailureRmiConnector extends RmiJmxConnector {
+ @Override
+ public void terminate(ProcessRef processRef, long timeoutMs) {
+ 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..620a59929fc
--- /dev/null
+++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.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.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);
+ assertThat(timeouts.getMonitorIsReadyTimeout()).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);
+ timeouts.setMonitorIsReadyTimeout(6L);
+
+ 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);
+ assertThat(timeouts.getMonitorIsReadyTimeout()).isEqualTo(6L);
+ }
+}
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/process/sonar-process/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
index 65b98c522da..65b98c522da 100644
--- a/server/process/sonar-process/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
diff --git a/server/process/sonar-process/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
index b33e179e5c8..b33e179e5c8 100644
--- a/server/process/sonar-process/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
diff --git a/server/process/sonar-process/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
index ab83e4adc03..ab83e4adc03 100644
--- a/server/process/sonar-process/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
diff --git a/server/process/sonar-process/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
index 23f5ecf5104..23f5ecf5104 100644
--- a/server/process/sonar-process/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
diff --git a/server/process/sonar-process/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
index 298193e01fa..298193e01fa 100644
--- a/server/process/sonar-process/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
diff --git a/server/process/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties b/server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties
index 1577a214b3b..1577a214b3b 100644
--- a/server/process/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties
+++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties
diff --git a/server/process/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties b/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties
index 5c06e58a32e..5c06e58a32e 100644
--- a/server/process/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties
+++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties
diff --git a/server/process/sonar-process/src/test/resources/sonar-dummy-app.jar b/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar
index 6dfd458329a..6dfd458329a 100644
--- a/server/process/sonar-process/src/test/resources/sonar-dummy-app.jar
+++ b/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar
Binary files differ
diff --git a/server/process/sonar-process/pom.xml b/server/sonar-process/pom.xml
index 9d812c9d868..25413fd7ce9 100644
--- a/server/process/sonar-process/pom.xml
+++ b/server/sonar-process/pom.xml
@@ -4,14 +4,13 @@
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>process</artifactId>
- <version>5.0-SNAPSHOT</version>
+ <artifactId>server</artifactId>
+ <version>4.5-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>sonar-process</artifactId>
- <packaging>jar</packaging>
<name>SonarQube :: Process</name>
<dependencies>
@@ -68,37 +67,42 @@
<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-dependency-plugin</artifactId>
- <version>2.8</version>
+ <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>
- <id>copy</id>
- <phase>process-test-resources</phase>
+ <phase>package</phase>
<goals>
- <goal>copy</goal>
+ <goal>single</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/AesCipher.java b/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java
index 204ae1a6b6b..204ae1a6b6b 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/AesCipher.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java b/server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java
index 5eb3eecd541..5eb3eecd541 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Cipher.java b/server/sonar-process/src/main/java/org/sonar/process/Cipher.java
index 4c437057757..4c437057757 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/Cipher.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/Cipher.java
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java b/server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java
index 355b6f885a2..b4f86457555 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java
@@ -47,7 +47,7 @@ public final class ConfigurationUtils {
return result;
}
- public static Props loadPropsFromCommandLineArgs(String[] args) {
+ 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)");
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Encryption.java b/server/sonar-process/src/main/java/org/sonar/process/Encryption.java
index cca05e6c780..cca05e6c780 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/Encryption.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/Encryption.java
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/JmxUtils.java b/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java
index 58ffb3af3e0..c0cf02bcc89 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/JmxUtils.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java
@@ -52,11 +52,12 @@ public class JmxUtils {
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(objectName(name))) {
- mbeanServer.unregisterMBean(objectName(name));
+ if (mbeanServer.isRegistered(oName)) {
+ mbeanServer.unregisterMBean(oName);
}
- mbeanServer.registerMBean(mbean, objectName(name));
+ mbeanServer.registerMBean(mbean, oName);
} catch (RuntimeException re) {
throw re;
} catch (Exception 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/process/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java b/server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java
index 17366850be3..2302a626d2c 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java
@@ -34,9 +34,9 @@ public class LoopbackAddress {
}
/**
- * Quite similar to InetAddress.getLoopbackAddress() which was introduced in Java 7. This
+ * 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 -Djava.net.preferIPv4Stack=true recommended for Elasticsearch
+ * support {@code -Djava.net.preferIPv4Stack=true} which is recommended for Elasticsearch.
*/
public static InetAddress get() {
if (instance == null) {
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/MessageException.java b/server/sonar-process/src/main/java/org/sonar/process/MessageException.java
index 5b86ef66c64..5b86ef66c64 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/MessageException.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/MessageException.java
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java b/server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java
index 2389fa5aa18..2389fa5aa18 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java
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..11ddf54beec
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java
@@ -0,0 +1,41 @@
+/*
+ * 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 process. No need to block until fully started and operational.
+ */
+ void start();
+
+ /**
+ * True if the process is started and operational (-> can accept requests), false if
+ * it's still starting. An exception is thrown is process failed to start (not starting
+ * nor started).
+ */
+ boolean isReady();
+
+ /**
+ * Blocks until the process is terminated
+ */
+ void awaitTermination();
+
+}
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java b/server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java
index 516c57c497a..074cb8cf5c2 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java
@@ -19,7 +19,6 @@
*/
package org.sonar.process;
-import java.io.IOException;
import java.net.ServerSocket;
public class NetworkUtils {
@@ -34,7 +33,7 @@ public class NetworkUtils {
int port = s.getLocalPort();
s.close();
return port;
- } catch (IOException e) {
+ } 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..7137a41fa12
--- /dev/null
+++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java
@@ -0,0 +1,155 @@
+/*
+ * 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();
+ boolean ready = false;
+ while (!ready) {
+ ready = monitoredProcess.isReady();
+ Thread.sleep(200L);
+ }
+ if (lifecycle.tryToMoveTo(State.STARTED)) {
+ monitoredProcess.awaitTermination();
+ }
+ } catch (Exception e) {
+ LoggerFactory.getLogger(getClass()).warn("Fail to start", e);
+
+ } 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/process/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java
index dacddd91847..dacddd91847 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java
index d212c08ccdd..3d024d420eb 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java
@@ -23,5 +23,6 @@ public interface ProcessMXBean extends Terminable {
boolean isReady();
- long ping();
+ void ping();
+
}
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java
index f399318aa1e..bdefa116949 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java
@@ -20,52 +20,51 @@
package org.sonar.process;
import org.apache.commons.io.IOUtils;
-import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Nullable;
-import java.lang.management.ManagementFactory;
-
public class ProcessUtils {
- private static final Logger LOGGER = LoggerFactory.getLogger(ProcessUtils.class);
-
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) {
- 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);
+ 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);
}
}
- }
-
- public static void addSelfShutdownHook(final Terminable terminable) {
- Thread shutdownHook = new Thread(new Runnable() {
- @Override
- public void run() {
- terminable.terminate();
- }
- });
- Runtime.getRuntime().addShutdownHook(shutdownHook);
+ return destroyed;
}
public static void closeStreams(@Nullable Process process) {
@@ -75,8 +74,4 @@ public class ProcessUtils {
IOUtils.closeQuietly(process.getErrorStream());
}
}
-
- public static boolean isJvmDebugEnabled() {
- return ManagementFactory.getRuntimeMXBean().getInputArguments().toString().indexOf("-agentlib:jdwp") > 0;
- }
}
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Props.java b/server/sonar-process/src/main/java/org/sonar/process/Props.java
index b868702eafc..b868702eafc 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/Props.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/Props.java
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/process/sonar-process/src/main/java/org/sonar/process/Terminable.java b/server/sonar-process/src/main/java/org/sonar/process/Terminable.java
index 109e91c42da..a8670609fe2 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/Terminable.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/Terminable.java
@@ -23,8 +23,6 @@ package org.sonar.process;
* This term "terminate" is used in order to not conflict with {@link Thread#stop()}.
*/
public interface Terminable {
- /**
- * Stops pending work. Must <b>not</b> throw an exception on error.
- */
+
void terminate();
}
diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/package-info.java b/server/sonar-process/src/main/java/org/sonar/process/package-info.java
index 09da5ce266d..09da5ce266d 100644
--- a/server/process/sonar-process/src/main/java/org/sonar/process/package-info.java
+++ b/server/sonar-process/src/main/java/org/sonar/process/package-info.java
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java b/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java
index 8350eafaa3e..8350eafaa3e 100644
--- a/server/process/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java b/server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java
index 2045cd4516d..2045cd4516d 100644
--- a/server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java
index de928b93850..de928b93850 100644
--- a/server/process/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java b/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java
index 0c11856b0fa..0c11856b0fa 100644
--- a/server/process/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java
index d2ab21157ef..599ea5d7a30 100644
--- a/server/process/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java
@@ -40,19 +40,20 @@ public class JmxUtilsTest {
class MyBean implements ProcessMXBean {
+
@Override
- public boolean isReady() {
- return false;
+ public void terminate() {
+
}
@Override
- public long ping() {
- return 0;
+ public void ping() {
+
}
@Override
- public void terminate() {
-
+ public boolean isReady() {
+ return true;
}
}
@@ -77,7 +78,6 @@ public class JmxUtilsTest {
@Test
public void testRegisterMBean() throws Exception {
-
// 0 Get mbServer and create out test MXBean
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
MyBean mxBean = new MyBean();
@@ -87,11 +87,6 @@ public class JmxUtilsTest {
assertThat(mbeanServer.isRegistered(objectName)).isFalse();
JmxUtils.registerMBean(mxBean, mxBean.getClass().getSimpleName());
assertThat(mbeanServer.isRegistered(objectName)).isTrue();
-
- // 2 assert that we can over-register
- assertThat(mbeanServer.isRegistered(objectName)).isTrue();
- JmxUtils.registerMBean(mxBean, mxBean.getClass().getSimpleName());
- assertThat(mbeanServer.isRegistered(objectName)).isTrue();
}
@Test
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/process/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java b/server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java
index 6a8819c3a81..6a8819c3a81 100644
--- a/server/process/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java b/server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java
index 83841c79952..83841c79952 100644
--- a/server/process/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java
index 09f6a597209..09f6a597209 100644
--- a/server/process/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java
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..03cd7409a49
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java
@@ -0,0 +1,234 @@
+/*
+ * 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 boolean isReady() {
+ return true;
+ }
+
+ @Override
+ public void awaitTermination() {
+
+ }
+
+ @Override
+ public void terminate() {
+
+ }
+ }
+
+ private static class StartupErrorProcess implements MonitoredProcess {
+
+ @Override
+ public void start() {
+ throw new IllegalStateException("ERROR");
+ }
+
+ @Override
+ public boolean isReady() {
+ return false;
+ }
+
+ @Override
+ public void awaitTermination() {
+
+ }
+
+ @Override
+ public void terminate() {
+
+ }
+ }
+}
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java
index e508f5a57f9..6f93126516e 100644
--- a/server/process/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java
@@ -25,8 +25,4 @@ import static org.fest.assertions.Assertions.assertThat;
public class ProcessUtilsTest {
- @Test
- public void isJvmDebugEnabled() {
- assertThat(ProcessUtils.isJvmDebugEnabled()).isFalse();
- }
}
diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/PropsTest.java b/server/sonar-process/src/test/java/org/sonar/process/PropsTest.java
index 6985b0ba086..5d283b44f8f 100644
--- a/server/process/sonar-process/src/test/java/org/sonar/process/PropsTest.java
+++ b/server/sonar-process/src/test/java/org/sonar/process/PropsTest.java
@@ -29,7 +29,7 @@ import static org.fest.assertions.Fail.fail;
public class PropsTest {
@Test
- public void value() throws Exception {
+ public void of() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "bar");
Props props = new Props(p);
@@ -41,7 +41,7 @@ public class PropsTest {
}
@Test
- public void valueAsInt() throws Exception {
+ public void intOf() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "33");
p.setProperty("blank", "");
@@ -56,7 +56,7 @@ public class PropsTest {
}
@Test
- public void valueAsInt_not_integer() throws Exception {
+ public void intOf_not_integer() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "bar");
Props props = new Props(p);
@@ -70,7 +70,7 @@ public class PropsTest {
}
@Test
- public void valueAsBoolean() throws Exception {
+ public void booleanOf() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "True");
p.setProperty("bar", "false");
@@ -82,7 +82,7 @@ public class PropsTest {
}
@Test
- public void valueAsBoolean_default_value() throws Exception {
+ public void booleanOf_default_value() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "true");
p.setProperty("bar", "false");
@@ -114,11 +114,9 @@ public class PropsTest {
Props props = new Props(p);
props.set("foo", "new_foo");
props.set("bar", "new_bar");
- props.set("null", null);
assertThat(props.value("foo")).isEqualTo("new_foo");
assertThat(props.value("bar")).isEqualTo("new_bar");
- assertThat(props.value("null")).isNull();
}
@Test
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..f6ca587a588
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.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.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;
+ private boolean ready = false;
+ // 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();
+ } catch (Exception e) {
+ throw new IllegalStateException("Fail to start Jetty", e);
+ }
+ }
+
+ @Override
+ public boolean isReady() {
+ if (ready) {
+ return true;
+ }
+ if (server.isStarted()) {
+ ready = true;
+ writeTimeToFile("readyAt");
+ }
+ return false;
+ }
+
+ @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..4c1a33221c6
--- /dev/null
+++ b/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.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.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 boolean isReady() {
+ return 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/pom.xml b/server/sonar-search/pom.xml
index e44acc5fdf6..f15dd473a3f 100644
--- a/server/sonar-search/pom.xml
+++ b/server/sonar-search/pom.xml
@@ -1,7 +1,5 @@
<?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">
+<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>
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..d2aa111e7e5 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,26 @@ 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
- }
+ @Override
+ public 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 +172,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 +216,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 +251,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/pom.xml b/server/sonar-server/pom.xml
index 3cf8474f9fe..188888b6367 100644
--- a/server/sonar-server/pom.xml
+++ b/server/sonar-server/pom.xml
@@ -1,6 +1,5 @@
<?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">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.codehaus.sonar</groupId>
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..01a5305c53d 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,62 +19,64 @@
*/
package org.sonar.server.app;
+import com.google.common.base.Throwables;
+import org.apache.catalina.LifecycleException;
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;
-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;
}
void start() {
- if (tomcat != null || hook != null) {
- throw new IllegalStateException("Server is already started");
- }
-
- try {
- // '%2F' (slash /) and '%5C' (backslash \) are permitted as path delimiters in URLs
- // See Ruby on Rails url_for
- System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
+ // '%2F' (slash /) and '%5C' (backslash \) are permitted as path delimiters in URLs
+ // See Ruby on Rails url_for
+ System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true");
- System.setProperty("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE", "true");
+ System.setProperty("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE", "true");
- tomcat = new Tomcat();
- // Initialize directories
- String basedir = tomcatBasedir().getAbsolutePath();
- tomcat.setBaseDir(basedir);
- tomcat.getHost().setAppBase(basedir);
- tomcat.getHost().setAutoDeploy(false);
- tomcat.getHost().setCreateDirs(false);
- tomcat.getHost().setDeployOnStartup(true);
- Logging.configure(tomcat, props);
- Connectors.configure(tomcat, props);
- StandardContext webappContext = Webapp.configure(tomcat, props);
- ProcessUtils.addSelfShutdownHook(this);
+ tomcat = new Tomcat();
+ // Initialize directories
+ String basedir = tomcatBasedir().getAbsolutePath();
+ tomcat.setBaseDir(basedir);
+ tomcat.getHost().setAppBase(basedir);
+ tomcat.getHost().setAutoDeploy(false);
+ tomcat.getHost().setCreateDirs(false);
+ tomcat.getHost().setDeployOnStartup(true);
+ Logging.configure(tomcat, props);
+ Connectors.configure(tomcat, props);
+ webappContext = Webapp.configure(tomcat, props);
+ try {
tomcat.start();
+ } catch (LifecycleException e) {
+ Throwables.propagate(e);
+ }
+ }
- 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();
+ boolean isReady() {
+ switch (webappContext.getState()) {
+ case NEW:
+ case INITIALIZING:
+ case INITIALIZED:
+ case STARTING_PREP:
+ case STARTING:
+ return false;
+ case STARTED:
+ return true;
+ default:
+ // problem, stopped or failed
+ throw new IllegalStateException("Webapp did not start");
}
}
@@ -82,25 +84,19 @@ class EmbeddedTomcat implements Terminable {
return new File(props.value("sonar.path.temp"), "tc");
}
- boolean isReady() {
- return ready && tomcat != null;
- }
-
- @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..87ffa5c5bee 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,32 @@ 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 boolean isReady() {
+ return tomcat.isReady();
+ }
+
+ @Override
+ 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/server/sonar-web/pom.xml b/server/sonar-web/pom.xml
index 0fcf1a9f973..324df14854a 100644
--- a/server/sonar-web/pom.xml
+++ b/server/sonar-web/pom.xml
@@ -1,6 +1,5 @@
<?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">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.codehaus.sonar</groupId>
diff --git a/sonar-application/pom.xml b/sonar-application/pom.xml
index 8e7f1f90012..94495baba59 100644
--- a/sonar-application/pom.xml
+++ b/sonar-application/pom.xml
@@ -1,6 +1,5 @@
<?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">
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
@@ -25,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>
@@ -237,8 +241,8 @@
<phase>package</phase>
<configuration>
<target>
- <checksum file="${project.build.directory}/sonarqube-${project.version}.zip" algorithm="md5"/>
- <checksum file="${project.build.directory}/sonarqube-${project.version}.zip" algorithm="sha"/>
+ <checksum file="${project.build.directory}/sonarqube-${project.version}.zip" algorithm="md5" />
+ <checksum file="${project.build.directory}/sonarqube-${project.version}.zip" algorithm="sha" />
</target>
</configuration>
<goals>
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..600828ec1db 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
+ 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);
}
}