Browse Source

SONAR-4898 refactor process monitoring

tags/4.5-RC2
Simon Brandhof 9 years ago
parent
commit
aeee283d21
93 changed files with 5650 additions and 281 deletions
  1. 2
    1
      server/pom.xml
  2. 2
    19
      server/process/sonar-process/pom.xml
  3. 23
    13
      server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java
  4. 23
    0
      server/process/sonar-process/src/test/java/org/sonar/process2/MonitorTest.java
  5. 87
    0
      server/sonar-process-monitor/pom.xml
  6. 175
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java
  7. 124
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java
  8. 35
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java
  9. 208
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java
  10. 60
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java
  11. 98
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java
  12. 133
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java
  13. 69
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java
  14. 78
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java
  15. 103
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java
  16. 70
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java
  17. 23
    0
      server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java
  18. 38
    0
      server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java
  19. 42
    0
      server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java
  20. 38
    0
      server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/InfiniteTerminationRmiConnector.java
  21. 79
    0
      server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java
  22. 43
    0
      server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java
  23. 442
    0
      server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java
  24. 31
    0
      server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java
  25. 53
    0
      server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java
  26. 52
    0
      server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java
  27. 1
    0
      server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt
  28. 1
    0
      server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt
  29. 3
    0
      server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt
  30. 1
    0
      server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt
  31. 1
    0
      server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml
  32. 212
    0
      server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties
  33. 3
    0
      server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties
  34. BIN
      server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar
  35. 108
    0
      server/sonar-process/pom.xml
  36. 133
    0
      server/sonar-process/src/main/java/org/sonar/process/AesCipher.java
  37. 35
    0
      server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java
  38. 27
    0
      server/sonar-process/src/main/java/org/sonar/process/Cipher.java
  39. 70
    0
      server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java
  40. 64
    0
      server/sonar-process/src/main/java/org/sonar/process/Encryption.java
  41. 81
    0
      server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java
  42. 56
    0
      server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java
  43. 71
    0
      server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java
  44. 36
    0
      server/sonar-process/src/main/java/org/sonar/process/MessageException.java
  45. 86
    0
      server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java
  46. 31
    0
      server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java
  47. 40
    0
      server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java
  48. 148
    0
      server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java
  49. 53
    0
      server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java
  50. 28
    0
      server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java
  51. 77
    0
      server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java
  52. 120
    0
      server/sonar-process/src/main/java/org/sonar/process/Props.java
  53. 26
    0
      server/sonar-process/src/main/java/org/sonar/process/State.java
  54. 57
    0
      server/sonar-process/src/main/java/org/sonar/process/StopperThread.java
  55. 52
    0
      server/sonar-process/src/main/java/org/sonar/process/SystemExit.java
  56. 28
    0
      server/sonar-process/src/main/java/org/sonar/process/Terminable.java
  57. 23
    0
      server/sonar-process/src/main/java/org/sonar/process/package-info.java
  58. 185
    0
      server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java
  59. 59
    0
      server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java
  60. 95
    0
      server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java
  61. 59
    0
      server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java
  62. 120
    0
      server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java
  63. 42
    0
      server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java
  64. 51
    0
      server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java
  65. 102
    0
      server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java
  66. 61
    0
      server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java
  67. 224
    0
      server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java
  68. 28
    0
      server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java
  69. 135
    0
      server/sonar-process/src/test/java/org/sonar/process/PropsTest.java
  70. 56
    0
      server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java
  71. 116
    0
      server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java
  72. 81
    0
      server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java
  73. 1
    0
      server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt
  74. 1
    0
      server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt
  75. 3
    0
      server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt
  76. 1
    0
      server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt
  77. 1
    0
      server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml
  78. 212
    0
      server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties
  79. 3
    0
      server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties
  80. BIN
      server/sonar-process/src/test/resources/sonar-dummy-app.jar
  81. 19
    0
      server/sonar-process/test-jar-with-dependencies.xml
  82. 36
    53
      server/sonar-search/src/main/java/org/sonar/search/SearchServer.java
  83. 3
    3
      server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java
  84. 48
    31
      server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java
  85. 11
    18
      server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java
  86. 0
    1
      server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java
  87. 3
    3
      server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java
  88. 5
    3
      server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java
  89. 19
    19
      server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java
  90. 1
    2
      server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java
  91. 2
    8
      server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java
  92. 5
    0
      sonar-application/pom.xml
  93. 59
    107
      sonar-application/src/main/java/org/sonar/application/App.java

+ 2
- 1
server/pom.xml View File

@@ -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>

+ 2
- 19
server/process/sonar-process/pom.xml View File

@@ -69,36 +69,19 @@
<scope>test</scope>
</dependency>
</dependencies>
<!--
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.8</version>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<id>copy</id>
<phase>process-test-resources</phase>
<goals>
<goal>copy</goal>
<goal>test-jar</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>org.codehaus.sonar</groupId>
<artifactId>sonar-dummy-app</artifactId>
<version>${project.version}</version>
<type>jar</type>
<outputDirectory>${project.build.testOutputDirectory}</outputDirectory>
<destFileName>sonar-dummy-app.jar</destFileName>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
-->
</project>

+ 23
- 13
server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java View File

@@ -35,27 +35,37 @@ public class ProcessUtils {
// only static stuff
}

/**
* Do not abuse to this method. It uses exceptions to get status.
* @return false if process is null or terminated, else true.
*/
public static boolean isAlive(@Nullable Process process) {
if (process == null) {
return false;
}
try {
process.exitValue();
return false;
} catch (IllegalThreadStateException e) {
LOGGER.trace("Process has no exit value yet", e);
return true;
boolean alive = false;
if (process != null) {
try {
process.exitValue();
} catch (IllegalThreadStateException ignored) {
alive = true;
}
}
return alive;
}

public static void destroyQuietly(@Nullable Process process) {
if (process != null && isAlive(process)) {
/**
* Destroys process (equivalent to kill -9) if alive
* @return true if the process was destroyed, false if process is null or already destroyed.
*/
public static boolean destroyQuietly(@Nullable Process process) {
boolean destroyed = false;
if (isAlive(process)) {
try {
process.destroy();
} catch (Exception ignored) {
LOGGER.warn("Exception while destroying the process", ignored);
destroyed = true;
} catch (Exception e) {
LoggerFactory.getLogger(ProcessUtils.class).error("Fail to destroy " + process);
}
}
return destroyed;
}

public static void addSelfShutdownHook(final Terminable terminable) {

+ 23
- 0
server/process/sonar-process/src/test/java/org/sonar/process2/MonitorTest.java View File

@@ -0,0 +1,23 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process2;

public class MonitorTest {
}

+ 87
- 0
server/sonar-process-monitor/pom.xml View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.codehaus.sonar</groupId>
<artifactId>server</artifactId>
<version>4.5-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sonar-process-monitor</artifactId>
<name>SonarQube :: Process Monitor</name>

<dependencies>
<dependency>
<groupId>org.codehaus.sonar</groupId>
<artifactId>sonar-process</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>

<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easytesting</groupId>
<artifactId>fest-assert</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.codehaus.sonar</groupId>
<artifactId>sonar-process</artifactId>
<type>test-jar</type>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>com.github.kevinsawicki</groupId>
<artifactId>http-request</artifactId>
<scope>test</scope>
</dependency>

</dependencies>
</project>

+ 175
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java View File

@@ -0,0 +1,175 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

import javax.annotation.Nullable;

import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;

public class JavaCommand {

// unique key among the group of commands to launch
private final String key;

private File workDir;

// any available port by default
private int jmxPort = -1;

// for example -Xmx1G
private final List<String> javaOptions = new ArrayList<String>();

// entry point
private String className;

// relative path to JAR files
private final List<String> classpath = new ArrayList<String>();

// program arguments (parameters of main(String[])
private final Map<String, String> arguments = new LinkedHashMap<String, String>();

private final Map<String, String> envVariables = new HashMap<String, String>(System.getenv());

public JavaCommand(String key) {
this.key = key;
}

public String getKey() {
return key;
}

public File getWorkDir() {
return workDir;
}

public JavaCommand setWorkDir(File workDir) {
this.workDir = workDir;
return this;
}

public JavaCommand setTempDir(File tempDir) {
this.javaOptions.add("-Djava.io.tmpdir=" + tempDir.getAbsolutePath());
return this;
}

public int getJmxPort() {
return jmxPort;
}

public JavaCommand setJmxPort(int jmxPort) {
this.jmxPort = jmxPort;
return this;
}

public List<String> getJavaOptions() {
return javaOptions;
}

public JavaCommand addJavaOption(String s) {
javaOptions.add(s);
return this;
}

public JavaCommand addJavaOptions(String s) {
Collections.addAll(javaOptions, s.split(" "));
return this;
}

public String getClassName() {
return className;
}

public JavaCommand setClassName(String className) {
this.className = className;
return this;
}

public List<String> getClasspath() {
return classpath;
}

public JavaCommand addClasspath(String s) {
classpath.add(s);
return this;
}

public Map<String, String> getArguments() {
return arguments;
}

public JavaCommand setArgument(String key, @Nullable String value) {
if (value == null) {
arguments.remove(key);
} else {
arguments.put(key, value);
}
return this;
}

public JavaCommand setArguments(Properties args) {
for (Map.Entry<Object, Object> entry : args.entrySet()) {
setArgument(entry.getKey().toString(), entry.getValue() != null ? entry.getValue().toString() : null);
}
return this;
}

public Map<String, String> getEnvVariables() {
return envVariables;
}

public JavaCommand setEnvVariable(String key, @Nullable String value) {
if (value == null) {
envVariables.remove(key);
} else {
envVariables.put(key, value);
}
return this;
}

public boolean isDebugMode() {
for (String javaOption : javaOptions) {
if (javaOption.contains("-agentlib:jdwp")) {
return true;
}
}
return false;
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder("JavaCommand{");
sb.append("workDir=").append(workDir);
sb.append(", jmxPort=").append(jmxPort);
sb.append(", javaOptions=").append(javaOptions);
sb.append(", className='").append(className).append('\'');
sb.append(", classpath=").append(classpath);
sb.append(", arguments=").append(arguments);
sb.append(", envVariables=").append(envVariables);
sb.append('}');
return sb.toString();
}
}

+ 124
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java View File

@@ -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);
}
}
}

+ 35
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java View File

@@ -0,0 +1,35 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

/**
* Interactions with monitored process
*/
public interface JmxConnector {

void connect(JavaCommand command, ProcessRef processRef);

void ping(ProcessRef process);

boolean isReady(ProcessRef process);

void terminate(ProcessRef process);

}

+ 208
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java View File

@@ -0,0 +1,208 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

import org.slf4j.LoggerFactory;
import org.sonar.process.Lifecycle;
import org.sonar.process.MessageException;
import org.sonar.process.State;
import org.sonar.process.SystemExit;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Monitor {

private final List<ProcessRef> processes = new CopyOnWriteArrayList<ProcessRef>();
private final TerminatorThread terminator;
private final JavaProcessLauncher launcher;
private final JmxConnector jmxConnector;
private final Lifecycle lifecycle = new Lifecycle();
private final Timeouts timeouts;

private final SystemExit systemExit;
private Thread shutdownHook = new Thread(new MonitorShutdownHook(), "Monitor Shutdown Hook");

// used by awaitTermination() to block until all processes are shutdown
private final List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<WatcherThread>();

Monitor(JavaProcessLauncher launcher, JmxConnector jmxConnector, Timeouts timeouts, SystemExit exit) {
this.launcher = launcher;
this.jmxConnector = jmxConnector;
this.timeouts = timeouts;
this.terminator = new TerminatorThread(processes, jmxConnector, timeouts);
this.systemExit = exit;
}

public static Monitor create() {
Timeouts timeouts = new Timeouts();
return new Monitor(new JavaProcessLauncher(timeouts), new RmiJmxConnector(timeouts),
timeouts, new SystemExit());
}

/**
* Starts commands and blocks current thread until all processes are in state {@link State#STARTED}.
* @throws java.lang.IllegalArgumentException if commands list is empty
* @throws java.lang.IllegalStateException if already started or if at least one process failed to start. In this case
* all processes are terminated. No need to execute {@link #stop()}
*/
public void start(List<JavaCommand> commands) {
if (commands.isEmpty()) {
throw new IllegalArgumentException("At least one command is required");
}

if (!lifecycle.tryToMoveTo(State.STARTING)) {
throw new IllegalStateException("Can not start multiple times");
}

// intercepts CTRL-C
Runtime.getRuntime().addShutdownHook(shutdownHook);

for (JavaCommand command : commands) {
try {
ProcessRef processRef = launcher.launch(command);
monitor(command, processRef);
} catch (RuntimeException e) {
// fail to start or to monitor
stop();
throw e;
}
}

if (!lifecycle.tryToMoveTo(State.STARTED)) {
// stopping or stopped during startup, for instance :
// 1. A is started
// 2. B starts
// 3. A crashes while B is starting
// 4. if B was not monitored during Terminator execution, then it's an alive orphan
stop();
throw new IllegalStateException("Stopped during startup");
}
}

private void monitor(JavaCommand command, ProcessRef processRef) {
// physically watch if process is alive
WatcherThread watcherThread = new WatcherThread(processRef, this);
watcherThread.start();
watcherThreads.add(watcherThread);

// add to list of monitored processes only when successfully connected to it
jmxConnector.connect(command, processRef);
processes.add(processRef);

// ping process on a regular basis
processRef.setPingEnabled(!command.isDebugMode());
if (processRef.isPingEnabled()) {
PingerThread.startPinging(processRef, jmxConnector, timeouts);
}

// wait for process to be ready (accept requests or so on)
waitForReady(processRef);

LoggerFactory.getLogger(getClass()).info(String.format("%s is up", processRef));
}

private void waitForReady(ProcessRef processRef) {
boolean ready = false;
while (!ready) {
if (processRef.isTerminated()) {
throw new MessageException(String.format("%s failed to start", processRef));
}
try {
ready = jmxConnector.isReady(processRef);
} catch (Exception ignored) {
// pb with the JMX connection, can occur if RMI not initialized yet
}
try {
Thread.sleep(300L);
} catch (InterruptedException e) {
throw new IllegalStateException("Interrupted while waiting for " + processRef + " to be ready", e);
}
}
}

/**
* Blocks until all processes are terminated
*/
public void awaitTermination() {
for (WatcherThread watcherThread : watcherThreads) {
while (watcherThread.isAlive()) {
try {
watcherThread.join();
} catch (InterruptedException ignored) {
// ignore, stop blocking
}
}
}
}

/**
* Blocks until all processes are terminated.
*/
public void stop() {
terminateAsync();
try {
terminator.join();
} catch (InterruptedException ignored) {
// ignore, stop blocking
}
// safeguard if TerminatorThread is buggy
hardKillAll();
lifecycle.tryToMoveTo(State.STOPPED);
systemExit.exit(0);
}

/**
* Asks for processes termination and returns without blocking until termination.
* @return true if termination was requested, false if it was already being terminated
*/
boolean terminateAsync() {
boolean requested = false;
if (lifecycle.tryToMoveTo(State.STOPPING)) {
requested = true;
terminator.start();
}
return requested;
}

private void hardKillAll() {
// no specific order, kill'em all!!!
for (ProcessRef process : processes) {
process.hardKill();
}
}

public State getState() {
return lifecycle.getState();
}

Thread getShutdownHook() {
return shutdownHook;
}

private class MonitorShutdownHook implements Runnable {
@Override
public void run() {
systemExit.setInShutdownHook();
// blocks until everything is corrected terminated
stop();
}
}
}

+ 60
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java View File

@@ -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);
}
}

+ 98
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java View File

@@ -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);
}
}

+ 133
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java View File

@@ -0,0 +1,133 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

import org.sonar.process.JmxUtils;
import org.sonar.process.LoopbackAddress;
import org.sonar.process.ProcessMXBean;
import org.sonar.process.ProcessUtils;

import javax.annotation.CheckForNull;
import javax.management.JMX;
import javax.management.MBeanServerConnection;
import javax.management.remote.JMXConnector;
import javax.management.remote.JMXConnectorFactory;
import javax.management.remote.JMXServiceURL;

import java.util.IdentityHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

class RmiJmxConnector implements JmxConnector {

static {
/*
Prevents such warnings :

WARNING: Failed to restart: java.io.IOException: Failed to get a RMI stub: javax.naming.ServiceUnavailableException [Root exception is java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
java.net.ConnectException: Connection refused]
Sep 11, 2014 7:32:32 PM RMIConnector RMIClientCommunicatorAdmin-doStop
WARNING: Failed to call the method close():java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is:
java.net.ConnectException: Connection refused
Sep 11, 2014 7:32:32 PM ClientCommunicatorAdmin Checker-run
WARNING: Failed to check connection: java.net.ConnectException: Connection refused
Sep 11, 2014 7:32:32 PM ClientCommunicatorAdmin Checker-run
WARNING: stopping
*/
System.setProperty("sun.rmi.transport.tcp.logLevel", "SEVERE");
}

private final Map<ProcessRef, ProcessMXBean> mbeans = new IdentityHashMap<ProcessRef, ProcessMXBean>();
private final Timeouts timeouts;

RmiJmxConnector(Timeouts timeouts) {
this.timeouts = timeouts;
}

@Override
public synchronized void connect(final JavaCommand command, ProcessRef processRef) {
ExecutorService executor = Executors.newSingleThreadExecutor();
ConnectorCallable callable = new ConnectorCallable(command, processRef.getProcess());
try {
Future<ProcessMXBean> future = executor.submit(callable);
ProcessMXBean mxBean = future.get(timeouts.getJmxConnectionTimeout(), TimeUnit.MILLISECONDS);
if (mxBean != null) {
mbeans.put(processRef, mxBean);
}
} catch (Exception e) {
if (callable.latestException != null) {
throw callable.latestException;
}
throw new IllegalStateException("Fail to connect to JMX", e);
} finally {
executor.shutdownNow();
}
}

@Override
public void ping(ProcessRef processRef) {
mbeans.get(processRef).ping();
}

@Override
public boolean isReady(ProcessRef processRef) {
return mbeans.get(processRef).isReady();
}

@Override
public void terminate(ProcessRef processRef) {
mbeans.get(processRef).terminate();
}

private static class ConnectorCallable implements Callable<ProcessMXBean> {
private final JavaCommand command;
private final Process process;
private RuntimeException latestException;

private ConnectorCallable(JavaCommand command, Process process) {
this.command = command;
this.process = process;
}

@Override
@CheckForNull
public ProcessMXBean call() throws Exception {
JMXServiceURL jmxUrl = JmxUtils.serviceUrl(LoopbackAddress.get(), command.getJmxPort());
while (ProcessUtils.isAlive(process)) {
try {
JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, null);
MBeanServerConnection mBeanServer = jmxConnector.getMBeanServerConnection();
return JMX.newMBeanProxy(mBeanServer, JmxUtils.objectName(command.getKey()), ProcessMXBean.class);
} catch (Exception e) {
latestException = new IllegalStateException(String.format(
"Fail to connect to JMX bean of %s [%s] ", command.getKey(), jmxUrl), e);
}
Thread.sleep(300L);
}

// process went down, no need to connect
return null;
}
}
}

+ 69
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java View File

@@ -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) {
}
}
}
}

+ 78
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java View File

@@ -0,0 +1,78 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

import org.slf4j.LoggerFactory;

import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

/**
* Terminates all monitored processes. Tries to gracefully terminate each process,
* then kill if timeout expires. Ping monitoring is disabled so process auto kills (self graceful termination, else self kill)
* if it does not receive the termination request.
*/
class TerminatorThread extends Thread {

private final List<ProcessRef> processes;
private final JmxConnector jmxConnector;
private final Timeouts timeouts;

TerminatorThread(List<ProcessRef> processes, JmxConnector jmxConnector, Timeouts timeouts) {
super("Terminator");
this.processes = processes;
this.jmxConnector = jmxConnector;
this.timeouts = timeouts;
}

@Override
public void run() {
// terminate in reverse order of startup (dependency order)
for (int index = processes.size() - 1; index >= 0; index--) {
final ProcessRef processRef = processes.get(index);
if (!processRef.isTerminated()) {
processRef.setPingEnabled(false);

ExecutorService executor = Executors.newSingleThreadExecutor();
Future future = executor.submit(new Runnable() {
@Override
public void run() {
// ask for graceful termination
LoggerFactory.getLogger(getClass()).info("Request termination of " + processRef);
jmxConnector.terminate(processRef);
}
});
try {
future.get(timeouts.getTerminationTimeout(), TimeUnit.MILLISECONDS);
} catch (Exception ignored) {
// failed to gracefully stop in a timely fashion
LoggerFactory.getLogger(getClass()).info(String.format("Kill %s", processRef));
} finally {
executor.shutdownNow();
// kill even if graceful termination was done, just to be sure that physical process is really down
processRef.hardKill();
}
}
}
}
}

+ 103
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java View File

@@ -0,0 +1,103 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

/**
* Most of the timeouts involved in process monitoring, in milliseconds
*/
class Timeouts {

private long terminationTimeout = 120000L;
private long jmxConnectionTimeout = 30000L;
private long monitorPingInterval = 3000L;
private long autokillPingTimeout = 60000L;
private long autokillPingInterval = 3000L;

/**
* [monitor] Timeout to get connected to RMI MXBean while process is alive
*/
long getJmxConnectionTimeout() {
return jmxConnectionTimeout;
}

/**
* @see #getJmxConnectionTimeout()
*/
void setJmxConnectionTimeout(long l) {
this.jmxConnectionTimeout = l;
}

/**
* [monitor] Delay between each ping request
*/
long getMonitorPingInterval() {
return monitorPingInterval;
}

/**
* @see #getMonitorPingInterval()
*/
void setMonitorPingInterval(long l) {
this.monitorPingInterval = l;
}

/**
* [monitored process] maximum age of last received ping before process autokills
*/
long getAutokillPingTimeout() {
return autokillPingTimeout;
}

/**
* @see #getAutokillPingTimeout()
*/
void setAutokillPingTimeout(long l) {
this.autokillPingTimeout = l;
}

/**
* [monitored process] delay between checks of freshness of received pings
*/
long getAutokillPingInterval() {
return autokillPingInterval;
}

/**
* @see #getAutokillPingInterval()
*/
void setAutokillPingInterval(long l) {
this.autokillPingInterval = l;
}

/**
* [both monitor and monitored process] timeout of graceful termination before hard killing
*/
long getTerminationTimeout() {
return terminationTimeout;
}

/**
* @see #getTerminationTimeout()
*/
void setTerminationTimeout(long l) {
this.terminationTimeout = l;
}

}

+ 70
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java View File

@@ -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();
}
}
}
}

+ 23
- 0
server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java View File

@@ -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;

+ 38
- 0
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java View File

@@ -0,0 +1,38 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

/**
* Used to verify that pings were sent or not.
*/
public class CallVerifierJmxConnector extends RmiJmxConnector {

boolean askedPing = false;

CallVerifierJmxConnector(Timeouts timeouts) {
super(timeouts);
}

@Override
public void ping(ProcessRef process) {
askedPing = true;
super.ping(process);
}
}

+ 42
- 0
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java View File

@@ -0,0 +1,42 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

public class ImpossibleToConnectJmxConnector implements JmxConnector {
@Override
public void connect(JavaCommand command, ProcessRef processRef) {
throw new IllegalStateException("Test - Impossible to connect to JMX");
}

@Override
public void ping(ProcessRef process) {

}

@Override
public boolean isReady(ProcessRef process) {
return false;
}

@Override
public void terminate(ProcessRef process) {

}
}

+ 38
- 0
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/InfiniteTerminationRmiConnector.java View File

@@ -0,0 +1,38 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

public class InfiniteTerminationRmiConnector extends RmiJmxConnector {

InfiniteTerminationRmiConnector(Timeouts timeouts) {
super(timeouts);
}

@Override
public void terminate(ProcessRef processRef) {
try {
while (true) {
Thread.sleep(50L);
}
} catch (Exception e) {

}
}
}

+ 79
- 0
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java View File

@@ -0,0 +1,79 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.File;
import java.util.Properties;

import static org.fest.assertions.Assertions.assertThat;

public class JavaCommandTest {

@Rule
public TemporaryFolder temp = new TemporaryFolder();

@Test
public void test_parameters() throws Exception {
JavaCommand command = new JavaCommand("es");

command.setArgument("first_arg", "val1");
Properties args = new Properties();
args.setProperty("second_arg", "val2");
command.setArguments(args);

command.setJmxPort(1234);
command.setClassName("org.sonar.ElasticSearch");
command.setEnvVariable("BUILD_ID", "1000");
File tempDir = temp.newFolder();
command.setTempDir(tempDir);
File workDir = temp.newFolder();
command.setWorkDir(workDir);
command.addClasspath("lib/*.jar");
command.addClasspath("conf/*.xml");
command.addJavaOption("-Xmx128m");

assertThat(command.toString()).isNotNull();
assertThat(command.getClasspath()).containsOnly("lib/*.jar", "conf/*.xml");
assertThat(command.getJavaOptions()).containsOnly("-Xmx128m", "-Djava.io.tmpdir=" + tempDir.getAbsolutePath());
assertThat(command.getWorkDir()).isSameAs(workDir);
assertThat(command.getJmxPort()).isEqualTo(1234);
assertThat(command.getClassName()).isEqualTo("org.sonar.ElasticSearch");
assertThat(command.getEnvVariables().get("BUILD_ID")).isEqualTo("1000");

// copy current env variables
assertThat(command.getEnvVariables().size()).isGreaterThan(1);
}

@Test
public void test_debug_mode() throws Exception {
JavaCommand command = new JavaCommand("es");
assertThat(command.isDebugMode()).isFalse();

command.addJavaOption("-Xmx512m");
assertThat(command.isDebugMode()).isFalse();

command.addJavaOption("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005");
assertThat(command.isDebugMode()).isTrue();
}
}

+ 43
- 0
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java View File

@@ -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");
}
}
}

+ 442
- 0
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java View File

@@ -0,0 +1,442 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

import com.github.kevinsawicki.http.HttpRequest;
import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.BeforeClass;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.Timeout;
import org.sonar.process.NetworkUtils;
import org.sonar.process.State;
import org.sonar.process.SystemExit;

import java.io.File;
import java.io.IOException;
import java.net.ConnectException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;

import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
import static org.mockito.Mockito.mock;

public class MonitorTest {

static File testJar;
Monitor monitor;
SystemExit exit = mock(SystemExit.class);

/**
* Find the JAR file containing the test apps. Classes can't be moved in sonar-process-monitor because
* they require sonar-process dependencies when executed here (sonar-process, commons-*, ...).
*/
@BeforeClass
public static void initTestJar() {
File targetDir = new File("server/sonar-process/target");
if (!targetDir.exists() || !targetDir.isDirectory()) {
targetDir = new File("../sonar-process/target");
}
if (!targetDir.exists() || !targetDir.isDirectory()) {
throw new IllegalStateException("target dir of sonar-process module not found. Please build it.");
}
Collection<File> jars = FileUtils.listFiles(targetDir, new String[] {"jar"}, false);
for (File jar : jars) {
if (jar.getName().startsWith("sonar-process-") && jar.getName().endsWith("-test-jar-with-dependencies.jar")) {
testJar = jar;
return;
}
}
throw new IllegalStateException("No sonar-process-*-test-jar-with-dependencies.jar in " + targetDir);
}

/**
* Safeguard
*/
@Rule
public Timeout globalTimeout = new Timeout(10000);

/**
* Temporary directory is used to interact with monitored processes, which write in it.
*/
@Rule
public TemporaryFolder temp = new TemporaryFolder();

/**
* Safeguard
*/
@After
public void tearDown() throws Exception {
try {
if (monitor != null) {
monitor.stop();
}
} catch (Throwable ignored) {
}
}

@Test
public void fail_to_start_if_no_commands() throws Exception {
monitor = newDefaultMonitor();
try {
monitor.start(Collections.<JavaCommand>emptyList());
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("At least one command is required");
}
}

@Test
public void fail_to_start_multiple_times() throws Exception {
monitor = newDefaultMonitor();
monitor.start(Arrays.asList(newStandardProcessCommand()));
boolean failed = false;
try {
monitor.start(Arrays.asList(newStandardProcessCommand()));
} catch (IllegalStateException e) {
failed = e.getMessage().equals("Can not start multiple times");
}
monitor.stop();
assertThat(failed);
}

@Test
public void start_then_stop_gracefully() throws Exception {
monitor = newDefaultMonitor();
HttpProcessClient client = new HttpProcessClient("test");
// blocks until started
monitor.start(Arrays.asList(client.newCommand()));

assertThat(client.isReady()).isTrue();
assertThat(client.wasReadyAt()).isLessThanOrEqualTo(System.currentTimeMillis());

// blocks until stopped
monitor.stop();
assertThat(client.isReady()).isFalse();
assertThat(client.wasGracefullyTerminated()).isTrue();
assertThat(monitor.getState()).isEqualTo(State.STOPPED);
}

@Test
public void start_then_stop_sequence_of_commands() throws Exception {
monitor = newDefaultMonitor();
HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2");
monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));

// start p2 when p1 is fully started (ready)
assertThat(p1.isReady()).isTrue();
assertThat(p2.isReady()).isTrue();
assertThat(p2.wasStartingAt()).isGreaterThanOrEqualTo(p1.wasReadyAt());

monitor.stop();

// stop in inverse order
assertThat(p1.isReady()).isFalse();
assertThat(p2.isReady()).isFalse();
assertThat(p1.wasGracefullyTerminated()).isTrue();
assertThat(p2.wasGracefullyTerminated()).isTrue();
assertThat(p2.wasGracefullyTerminatedAt()).isLessThanOrEqualTo(p1.wasGracefullyTerminatedAt());
}

@Test
public void fail_to_connect_to_jmx() throws Exception {
Timeouts timeouts = new Timeouts();
monitor = new Monitor(new JavaProcessLauncher(timeouts),
new ImpossibleToConnectJmxConnector(), timeouts, exit);

HttpProcessClient p1 = new HttpProcessClient("p1");
try {
monitor.start(Arrays.asList(p1.newCommand()));
fail();
} catch (Exception e) {
// process was correctly launched, but there was a problem with RMI
assertThat(p1.isReady()).isFalse();
assertThat(p1.wasGracefullyTerminated()).isFalse();
}
}

@Test
public void terminate_all_processes_if_monitor_shutdowns() throws Exception {
monitor = newDefaultMonitor();
HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2");
monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
assertThat(p1.isReady()).isTrue();
assertThat(p2.isReady()).isTrue();

// emulate CTRL-C
monitor.getShutdownHook().run();
monitor.getShutdownHook().join();

assertThat(p1.wasGracefullyTerminated()).isTrue();
assertThat(p2.wasGracefullyTerminated()).isTrue();
}

@Test
public void terminate_all_processes_if_one_monitored_process_shutdowns() throws Exception {
monitor = newDefaultMonitor();
HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2");
monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
assertThat(p1.isReady()).isTrue();
assertThat(p2.isReady()).isTrue();

// kill p1 -> waiting for detection by monitor than termination of p2
p1.kill();
monitor.awaitTermination();

assertThat(p1.isReady()).isFalse();
assertThat(p2.isReady()).isFalse();
assertThat(p1.wasGracefullyTerminated()).isFalse();
assertThat(p2.wasGracefullyTerminated()).isTrue();
}

@Test
public void terminate_all_processes_if_one_fails_to_start() throws Exception {
monitor = newDefaultMonitor();
HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2", -1, NetworkUtils.freePort());
try {
monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
fail();
} catch (Exception expected) {
assertThat(p1.wasReady()).isTrue();
assertThat(p2.wasReady()).isFalse();
assertThat(p1.wasGracefullyTerminated()).isTrue();
// self "gracefully terminated", even if startup went bad
assertThat(p2.wasGracefullyTerminated()).isTrue();
}
}

@Test
public void kill_process_if_too_long_to_request_gracefully_termination() throws Exception {
Timeouts timeouts = new Timeouts();
timeouts.setTerminationTimeout(100L);
monitor = new Monitor(new JavaProcessLauncher(timeouts),
new InfiniteTerminationRmiConnector(timeouts), timeouts, exit);

HttpProcessClient p1 = new HttpProcessClient("p1");
monitor.start(Arrays.asList(p1.newCommand()));
assertThat(p1.isReady()).isTrue();

monitor.stop();
assertThat(p1.isReady()).isFalse();
}

@Test
public void kill_process_if_fail_to_request_gracefully_termination() throws Exception {
Timeouts timeouts = new Timeouts();
timeouts.setTerminationTimeout(100L);
monitor = new Monitor(new JavaProcessLauncher(timeouts),
new TerminationFailureRmiConnector(timeouts), timeouts, exit);

HttpProcessClient p1 = new HttpProcessClient("p1");
monitor.start(Arrays.asList(p1.newCommand()));
assertThat(p1.isReady()).isTrue();

monitor.stop();
assertThat(p1.isReady()).isFalse();
}

@Test
public void fail_to_start_if_bad_class_name() throws Exception {
monitor = newDefaultMonitor();
JavaCommand command = new JavaCommand("test")
.addClasspath(testJar.getAbsolutePath())
.setClassName("org.sonar.process.test.Unknown")
.setJmxPort(NetworkUtils.freePort())
.setTempDir(temp.newFolder());

try {
monitor.start(Arrays.asList(command));
fail();
} catch (Exception e) {
// expected
// TODO improve, too many stacktraces logged
}
}

@Test
public void terminate_all_if_one_monitored_process_shutdowns() throws Exception {
monitor = newDefaultMonitor();
HttpProcessClient client = new HttpProcessClient("test");
// blocks until started
monitor.start(Arrays.asList(client.newCommand()));
assertThat(client.isReady()).isTrue();

client.kill();
assertThat(client.isReady()).isFalse();

// does not wait, already terminated
monitor.awaitTermination();

// TODO check logs
}

@Test
public void fail_if_jmx_port_is_not_available() throws Exception {
monitor = newDefaultMonitor();
// c1 and c2 have same JMX port
int jmxPort = NetworkUtils.freePort();
HttpProcessClient p1 = new HttpProcessClient("p1", NetworkUtils.freePort(), jmxPort);
HttpProcessClient p2 = new HttpProcessClient("p2", NetworkUtils.freePort(), jmxPort);
try {
monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand()));
fail();
} catch (Exception expected) {
assertThat(p1.wasReady()).isTrue();
assertThat(p2.wasReady()).isFalse();
assertThat(p1.isReady()).isFalse();
assertThat(p2.isReady()).isFalse();
}
}

@Test
public void disable_autokill_on_jvm_debug_mode() throws Exception {
Timeouts timeouts = new Timeouts();
timeouts.setMonitorPingInterval(10L);
timeouts.setAutokillPingInterval(10L);
timeouts.setAutokillPingTimeout(10L);
CallVerifierJmxConnector jmxConnector = new CallVerifierJmxConnector(timeouts);
monitor = new Monitor(new JavaProcessLauncher(timeouts), jmxConnector, timeouts, exit);

JavaCommand command = newStandardProcessCommand()
.addJavaOption("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=" + NetworkUtils.freePort());
monitor.start(Arrays.asList(command));

Thread.sleep(20L);
assertThat(jmxConnector.askedPing).isFalse();

monitor.stop();
}

private Monitor newDefaultMonitor() {
Timeouts timeouts = new Timeouts();
return new Monitor(new JavaProcessLauncher(timeouts), new RmiJmxConnector(timeouts), timeouts, exit);
}

/**
* Interaction with {@link org.sonar.process.test.HttpProcess}
*/
private class HttpProcessClient {
private final int httpPort;
private final String commandKey;
private final File tempDir;
private int jmxPort;

private HttpProcessClient(String commandKey) throws IOException {
this(commandKey, NetworkUtils.freePort(), NetworkUtils.freePort());
}

/**
* Use httpPort=-1 to make server fail to start
*/
private HttpProcessClient(String commandKey, int httpPort, int jmxPort) throws IOException {
this.commandKey = commandKey;
this.tempDir = temp.newFolder(commandKey);
this.httpPort = httpPort;
this.jmxPort = jmxPort;
}

JavaCommand newCommand() throws IOException {
return new JavaCommand(commandKey)
.addClasspath(testJar.getAbsolutePath())
.setClassName("org.sonar.process.test.HttpProcess")
.setJmxPort(jmxPort)
.setArgument("httpPort", String.valueOf(httpPort))
.setTempDir(tempDir);
}

/**
* @see org.sonar.process.test.HttpProcess
*/
boolean isReady() {
try {
HttpRequest httpRequest = HttpRequest.get("http://localhost:" + httpPort + "/ping")
.readTimeout(500).connectTimeout(500);
return httpRequest.ok() && httpRequest.body().equals("ping");
} catch (HttpRequest.HttpRequestException e) {
if (e.getCause() instanceof ConnectException) {
return false;
}
throw new IllegalStateException("Fail to know the process status", e);
}
}

/**
* @see org.sonar.process.test.HttpProcess
*/
void kill() {
try {
HttpRequest.post("http://localhost:" + httpPort + "/kill")
.readTimeout(500).connectTimeout(500).ok();
} catch (Exception e) {
// HTTP request can't be fully processed, as web server hardly
// calls "System.exit()"
}
}

/**
* @see org.sonar.process.test.HttpProcess
*/
boolean wasGracefullyTerminated() {
return fileExists("terminatedAt");
}

long wasStartingAt() throws IOException {
return readTimeFromFile("startingAt");
}

long wasGracefullyTerminatedAt() throws IOException {
return readTimeFromFile("terminatedAt");
}

boolean wasReady() throws IOException {
return fileExists("readyAt");
}

long wasReadyAt() throws IOException {
return readTimeFromFile("readyAt");
}

private long readTimeFromFile(String filename) throws IOException {
File file = new File(tempDir, filename);
if (file.isFile() && file.exists()) {
return Long.parseLong(FileUtils.readFileToString(file));
}
throw new IllegalStateException("File does not exist");
}

private boolean fileExists(String filename) {
File file = new File(tempDir, filename);
return file.isFile() && file.exists();
}
}

private JavaCommand newStandardProcessCommand() throws IOException {
return new JavaCommand("standard")
.addClasspath(testJar.getAbsolutePath())
.setClassName("org.sonar.process.test.StandardProcess")
.setJmxPort(NetworkUtils.freePort())
.setTempDir(temp.newFolder());
}

}

+ 31
- 0
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java View File

@@ -0,0 +1,31 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

public class TerminationFailureRmiConnector extends RmiJmxConnector {
TerminationFailureRmiConnector(Timeouts timeouts) {
super(timeouts);
}

@Override
public void terminate(ProcessRef processRef) {
throw new IllegalStateException("Test - fail to send termination request");
}
}

+ 53
- 0
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java View File

@@ -0,0 +1,53 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.monitor;

import org.junit.Test;

import static org.fest.assertions.Assertions.assertThat;

public class TimeoutsTest {

@Test
public void test_default_values() throws Exception {
Timeouts timeouts = new Timeouts();
assertThat(timeouts.getMonitorPingInterval()).isGreaterThan(1000L);
assertThat(timeouts.getAutokillPingInterval()).isGreaterThan(1000L);
assertThat(timeouts.getAutokillPingTimeout()).isGreaterThan(1000L);
assertThat(timeouts.getTerminationTimeout()).isGreaterThan(1000L);
assertThat(timeouts.getJmxConnectionTimeout()).isGreaterThan(1000L);
}

@Test
public void test_values() throws Exception {
Timeouts timeouts = new Timeouts();
timeouts.setAutokillPingInterval(1L);
timeouts.setAutokillPingTimeout(2L);
timeouts.setTerminationTimeout(3L);
timeouts.setJmxConnectionTimeout(4L);
timeouts.setMonitorPingInterval(5L);

assertThat(timeouts.getAutokillPingInterval()).isEqualTo(1L);
assertThat(timeouts.getAutokillPingTimeout()).isEqualTo(2L);
assertThat(timeouts.getTerminationTimeout()).isEqualTo(3L);
assertThat(timeouts.getJmxConnectionTimeout()).isEqualTo(4L);
assertThat(timeouts.getMonitorPingInterval()).isEqualTo(5L);
}
}

+ 52
- 0
server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java View File

@@ -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();
}
}

+ 1
- 0
server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt View File

@@ -0,0 +1 @@
0PZz+G+f8mjr3sPn4+AhHg==

+ 1
- 0
server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt View File

@@ -0,0 +1 @@
badbadbad==

+ 3
- 0
server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt View File

@@ -0,0 +1,3 @@

0PZz+G+f8mjr3sPn4+AhHg==


+ 1
- 0
server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt View File

@@ -0,0 +1 @@
IBxEUxZ41c8XTxyaah1Qlg==

+ 1
- 0
server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml View File

@@ -0,0 +1 @@
<configuration/>

+ 212
- 0
server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties View File

@@ -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

+ 3
- 0
server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties View File

@@ -0,0 +1,3 @@
hello: world
foo=bar
java.io.tmpdir=/should/be/overridden

BIN
server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar View File


+ 108
- 0
server/sonar-process/pom.xml View File

@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.codehaus.sonar</groupId>
<artifactId>server</artifactId>
<version>4.5-SNAPSHOT</version>
<relativePath>../</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>sonar-process</artifactId>
<name>SonarQube :: Process</name>

<dependencies>

<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>

<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.easytesting</groupId>
<artifactId>fest-assert</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-all</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<configuration>
<descriptors>
<descriptor>test-jar-with-dependencies.xml</descriptor>
</descriptors>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

+ 133
- 0
server/sonar-process/src/main/java/org/sonar/process/AesCipher.java View File

@@ -0,0 +1,133 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

package org.sonar.process;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;

import javax.annotation.Nullable;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.IOException;
import java.security.Key;
import java.security.SecureRandom;

final class AesCipher implements Cipher {

// Can't be increased because of Java 6 policy files :
// https://confluence.terena.org/display/~visser/No+256+bit+ciphers+for+Java+apps
// http://java.sun.com/javase/6/webnotes/install/jre/README
public static final int KEY_SIZE_IN_BITS = 128;

private static final String CRYPTO_KEY = "AES";

/**
* Duplication from CoreProperties.ENCRYPTION_SECRET_KEY_PATH
*/
static final String ENCRYPTION_SECRET_KEY_PATH = "sonar.secretKeyPath";

private String pathToSecretKey;

AesCipher(@Nullable String pathToSecretKey) {
this.pathToSecretKey = pathToSecretKey;
}

@Override
public String encrypt(String clearText) {
try {
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY);
cipher.init(javax.crypto.Cipher.ENCRYPT_MODE, loadSecretFile());
return new String(Base64.encodeBase64(cipher.doFinal(clearText.getBytes("UTF-8"))));
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException(e);
}
}

@Override
public String decrypt(String encryptedText) {
try {
javax.crypto.Cipher cipher = javax.crypto.Cipher.getInstance(CRYPTO_KEY);
cipher.init(javax.crypto.Cipher.DECRYPT_MODE, loadSecretFile());
byte[] cipherData = cipher.doFinal(Base64.decodeBase64(StringUtils.trim(encryptedText)));
return new String(cipherData);
} catch (RuntimeException e) {
throw e;
} catch (Exception e) {
throw new IllegalStateException(e);
}
}

/**
* This method checks the existence of the file, but not the validity of the contained key.
*/
boolean hasSecretKey() {
String path = getPathToSecretKey();
if (StringUtils.isNotBlank(path)) {
File file = new File(path);
return file.exists() && file.isFile();
}
return false;
}

private Key loadSecretFile() throws IOException {
String path = getPathToSecretKey();
return loadSecretFileFromFile(path);
}

Key loadSecretFileFromFile(String path) throws IOException {
if (StringUtils.isBlank(path)) {
throw new IllegalStateException("Secret key not found. Please set the property " + ENCRYPTION_SECRET_KEY_PATH);
}
File file = new File(path);
if (!file.exists() || !file.isFile()) {
throw new IllegalStateException("The property " + ENCRYPTION_SECRET_KEY_PATH + " does not link to a valid file: " + path);
}
String s = FileUtils.readFileToString(file);
if (StringUtils.isBlank(s)) {
throw new IllegalStateException("No secret key in the file: " + path);
}
return new SecretKeySpec(Base64.decodeBase64(StringUtils.trim(s)), CRYPTO_KEY);
}

String generateRandomSecretKey() {
try {
KeyGenerator keyGen = KeyGenerator.getInstance(CRYPTO_KEY);
keyGen.init(KEY_SIZE_IN_BITS, new SecureRandom());
SecretKey secretKey = keyGen.generateKey();
return new String(Base64.encodeBase64(secretKey.getEncoded()));

} catch (Exception e) {
throw new IllegalStateException("Fail to generate secret key", e);
}
}

String getPathToSecretKey() {
if (StringUtils.isBlank(pathToSecretKey)) {
pathToSecretKey = new File(FileUtils.getUserDirectoryPath(), ".sonar/sonar-secret.txt").getPath();
}
return pathToSecretKey;
}
}

+ 35
- 0
server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java View File

@@ -0,0 +1,35 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

package org.sonar.process;

import org.apache.commons.codec.binary.Base64;

final class Base64Cipher implements Cipher {
@Override
public String encrypt(String clearText) {
return new String(Base64.encodeBase64(clearText.getBytes()));
}

@Override
public String decrypt(String encryptedText) {
return new String(Base64.decodeBase64(encryptedText));
}
}

+ 27
- 0
server/sonar-process/src/main/java/org/sonar/process/Cipher.java View File

@@ -0,0 +1,27 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

package org.sonar.process;

interface Cipher {
String encrypt(String clearText);

String decrypt(String encryptedText);
}

+ 70
- 0
server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java View File

@@ -0,0 +1,70 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.text.StrSubstitutor;

import java.io.File;
import java.io.FileReader;
import java.util.Enumeration;
import java.util.Map;
import java.util.Properties;

public final class ConfigurationUtils {

private ConfigurationUtils() {
// Utility class
}

public static Properties interpolateVariables(Properties properties, Map<String, String> variables) {
Properties result = new Properties();
Enumeration keys = properties.keys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
String value = (String) properties.get(key);
String interpolatedValue = StrSubstitutor.replace(value, variables, "${env:", "}");
result.setProperty(key, interpolatedValue);
}
return result;
}

static Props loadPropsFromCommandLineArgs(String[] args) {
if (args.length != 1) {
throw new IllegalArgumentException("Only a single command-line argument is accepted " +
"(absolute path to configuration file)");
}

File propertyFile = new File(args[0]);
Properties properties = new Properties();
FileReader reader = null;
try {
reader = new FileReader(propertyFile);
properties.load(reader);
} catch (Exception e) {
throw new IllegalStateException("Could not read properties from file: " + args[0], e);
} finally {
IOUtils.closeQuietly(reader);
FileUtils.deleteQuietly(propertyFile);
}
return new Props(properties);
}
}

+ 64
- 0
server/sonar-process/src/main/java/org/sonar/process/Encryption.java View File

@@ -0,0 +1,64 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

package org.sonar.process;

import javax.annotation.Nullable;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* @since 3.0
*/
public final class Encryption {

private static final String BASE64_ALGORITHM = "b64";

private static final String AES_ALGORITHM = "aes";
private final AesCipher aesCipher;

private final Map<String, Cipher> ciphers = new HashMap<String, Cipher>();
private static final Pattern ENCRYPTED_PATTERN = Pattern.compile("\\{(.*?)\\}(.*)");

public Encryption(@Nullable String pathToSecretKey) {
aesCipher = new AesCipher(pathToSecretKey);
ciphers.put(BASE64_ALGORITHM, new Base64Cipher());
ciphers.put(AES_ALGORITHM, aesCipher);
}

public boolean isEncrypted(String value) {
return value.indexOf('{') == 0 && value.indexOf('}') > 1;
}

public String decrypt(String encryptedText) {
Matcher matcher = ENCRYPTED_PATTERN.matcher(encryptedText);
if (matcher.matches()) {
Cipher cipher = ciphers.get(matcher.group(1).toLowerCase(Locale.ENGLISH));
if (cipher != null) {
return cipher.decrypt(matcher.group(2));
}
}
return encryptedText;
}

}

+ 81
- 0
server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java View File

@@ -0,0 +1,81 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import javax.management.MBeanServer;
import javax.management.MalformedObjectNameException;
import javax.management.ObjectName;
import javax.management.remote.JMXServiceURL;

import java.lang.management.ManagementFactory;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.MalformedURLException;

public class JmxUtils {

private JmxUtils() {
// only static stuff
}

public static final String DOMAIN = "org.sonar";
public static final String NAME_PROPERTY = "name";

public static final String WEB_SERVER_NAME = "web";
public static final String SEARCH_SERVER_NAME = "search";

public static ObjectName objectName(String name) {
try {
return new ObjectName(DOMAIN, NAME_PROPERTY, name);
} catch (MalformedObjectNameException e) {
throw new IllegalStateException("Cannot create ObjectName for " + name, e);
}
}

public static void registerMBean(Object mbean, String name) {
try {
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
ObjectName oName = objectName(name);
// Check if already registered in JVM (might run multiple instance in JUnits)
if (mbeanServer.isRegistered(oName)) {
mbeanServer.unregisterMBean(oName);
}
mbeanServer.registerMBean(mbean, oName);
} catch (RuntimeException re) {
throw re;
} catch (Exception e) {
throw new IllegalStateException("Fail to register JMX MBean named " + name, e);
}
}

public static JMXServiceURL serviceUrl(InetAddress host, int port) {
String address = host.getHostAddress();
if (host instanceof Inet6Address) {
// See http://docs.oracle.com/javase/7/docs/api/javax/management/remote/JMXServiceURL.html
// "The host is a host name, an IPv4 numeric host address, or an IPv6 numeric address enclosed in square brackets."
address = String.format("[%s]", address);
}
try {
return new JMXServiceURL("rmi", address, port, String.format("/jndi/rmi://%s:%d/jmxrmi", address, port));
} catch (MalformedURLException e) {
throw new IllegalStateException("JMX url does not look well formed", e);
}
}
}

+ 56
- 0
server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java View File

@@ -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();
}
}

+ 71
- 0
server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java View File

@@ -0,0 +1,71 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import java.net.Inet4Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;

public class LoopbackAddress {

private static InetAddress instance;

private LoopbackAddress() {
// only static stuff
}

/**
* Quite similar to {@code InetAddress.getLoopbackAddress()} which was introduced in Java 7. This
* method aims to support Java 6. It returns an IPv4 address, but not IPv6 in order to
* support {@code -Djava.net.preferIPv4Stack=true} which is recommended for Elasticsearch.
*/
public static InetAddress get() {
if (instance == null) {
try {
instance = doGet(NetworkInterface.getNetworkInterfaces());
} catch (SocketException e) {
throw new IllegalStateException("Fail to browse network interfaces", e);
}

}
return instance;
}

static InetAddress doGet(Enumeration<NetworkInterface> ifaces) {
InetAddress result = null;
while (ifaces.hasMoreElements() && result == null) {
NetworkInterface iface = ifaces.nextElement();
Enumeration<InetAddress> addresses = iface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (addr.isLoopbackAddress() && addr instanceof Inet4Address) {
result = addr;
break;
}
}
}
if (result == null) {
throw new IllegalStateException("Impossible to get a IPv4 loopback address");
}
return result;
}
}

+ 36
- 0
server/sonar-process/src/main/java/org/sonar/process/MessageException.java View File

@@ -0,0 +1,36 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

public class MessageException extends RuntimeException {
public MessageException(String message) {
super(message);
}

/**
* Does not fill in the stack trace
*
* @see Throwable#fillInStackTrace()
*/
@Override
public synchronized Throwable fillInStackTrace() {
return this;
}
}

+ 86
- 0
server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java View File

@@ -0,0 +1,86 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang.StringUtils;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class MinimumViableSystem {

private final Map<String, String> requiredJavaOptions = new HashMap<String, String>();

public MinimumViableSystem setRequiredJavaOption(String propertyKey, String expectedValue) {
requiredJavaOptions.put(propertyKey, expectedValue);
return this;
}

/**
* Entry point for all checks
*/
public void check() {
checkJavaVersion();
checkJavaOptions();
checkWritableTempDir();
}

/**
* Verify that temp directory is writable
*/
private void checkWritableTempDir() {
checkWritableDir(System.getProperty("java.io.tmpdir"));
}

void checkWritableDir(String tempPath) {
try {
File tempFile = File.createTempFile("check", "tmp", new File(tempPath));
FileUtils.deleteQuietly(tempFile);
} catch (IOException e) {
throw new IllegalStateException(String.format("Temp directory is not writable: %s", tempPath), e);
}
}

void checkJavaOptions() {
for (Map.Entry<String, String> entry : requiredJavaOptions.entrySet()) {
String value = System.getProperty(entry.getKey());
if (!StringUtils.equals(value, entry.getValue())) {
throw new MessageException(String.format(
"JVM option '%s' must be set to '%s'. Got '%s'", entry.getKey(), entry.getValue(), StringUtils.defaultString(value)));
}
}
}

void checkJavaVersion() {
String javaVersion = System.getProperty("java.specification.version");
checkJavaVersion(javaVersion);
}

void checkJavaVersion(String javaVersion) {
if (!javaVersion.startsWith("1.6") && !javaVersion.startsWith("1.7") && !javaVersion.startsWith("1.8")) {
// still better than "java.lang.UnsupportedClassVersionError: Unsupported major.minor version 49.0
throw new MessageException(String.format("Supported versions of Java are 1.6, 1.7 and 1.8. Got %s.", javaVersion));
}
}

}

+ 31
- 0
server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java View File

@@ -0,0 +1,31 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

public interface MonitoredProcess extends Terminable {

/**
* Starts and blocks until ready
*/
void start();

void awaitTermination();

}

+ 40
- 0
server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java View File

@@ -0,0 +1,40 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import java.net.ServerSocket;

public class NetworkUtils {

private NetworkUtils() {
// only static stuff
}

public static int freePort() {
try {
ServerSocket s = new ServerSocket(0);
int port = s.getLocalPort();
s.close();
return port;
} catch (Exception e) {
throw new IllegalStateException("Can not find an open network port", e);
}
}
}

+ 148
- 0
server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java View File

@@ -0,0 +1,148 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.slf4j.LoggerFactory;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class ProcessEntryPoint implements ProcessMXBean {

public static final String PROPERTY_PROCESS_KEY = "process.key";
public static final String PROPERTY_AUTOKILL_DISABLED = "process.autokill.disabled";
public static final String PROPERTY_AUTOKILL_PING_TIMEOUT = "process.autokill.pingTimeout";
public static final String PROPERTY_AUTOKILL_PING_INTERVAL = "process.autokill.pingInterval";
public static final String PROPERTY_TERMINATION_TIMEOUT = "process.terminationTimeout";

private final Props props;
private final Lifecycle lifecycle = new Lifecycle();
private volatile MonitoredProcess monitoredProcess;
private volatile long lastPing = 0L;
private volatile StopperThread stopperThread;
private final SystemExit exit;
private Thread shutdownHook = new Thread(new Runnable() {
@Override
public void run() {
exit.setInShutdownHook();
terminate();
}
});

ProcessEntryPoint(Props props, SystemExit exit) {
this.props = props;
this.exit = exit;
}

public Props getProps() {
return props;
}

/**
* Launch process and waits until it's down
*/
public void launch(MonitoredProcess mp) {
if (!lifecycle.tryToMoveTo(State.STARTING)) {
throw new IllegalStateException("Already started");
}
monitoredProcess = mp;

// TODO check if these properties are available in System Info
JmxUtils.registerMBean(this, props.nonNullValue(PROPERTY_PROCESS_KEY));
Runtime.getRuntime().addShutdownHook(shutdownHook);
if (!props.valueAsBoolean(PROPERTY_AUTOKILL_DISABLED, false)) {
// mainly for Java Debugger
scheduleAutokill();
}

try {
monitoredProcess.start();
if (lifecycle.tryToMoveTo(State.STARTED)) {
monitoredProcess.awaitTermination();
}
} catch (Exception ignored) {
} finally {
terminate();
}
}

@Override
public boolean isReady() {
return lifecycle.getState() == State.STARTED;
}

@Override
public void ping() {
lastPing = System.currentTimeMillis();
}

/**
* Blocks until stopped in a timely fashion (see {@link org.sonar.process.StopperThread})
*/
@Override
public void terminate() {
if (lifecycle.tryToMoveTo(State.STOPPING)) {
stopperThread = new StopperThread(monitoredProcess, Long.parseLong(props.nonNullValue(PROPERTY_TERMINATION_TIMEOUT)));
stopperThread.start();
}
try {
// stopperThread is not null for sure
// join() does nothing if thread already finished
stopperThread.join();
lifecycle.tryToMoveTo(State.STOPPED);
} catch (InterruptedException e) {
// nothing to do, the process is going to be exited
}
exit.exit(0);
}

private void scheduleAutokill() {
final long autokillPingTimeoutMs = props.valueAsInt(PROPERTY_AUTOKILL_PING_TIMEOUT);
long autokillPingIntervalMs = props.valueAsInt(PROPERTY_AUTOKILL_PING_INTERVAL);
Runnable autokiller = new Runnable() {
@Override
public void run() {
long time = System.currentTimeMillis();
if (time - lastPing > autokillPingTimeoutMs) {
LoggerFactory.getLogger(getClass()).info(String.format(
"Did not receive any ping during %d seconds. Shutting down.", autokillPingTimeoutMs / 1000));
terminate();
}
}
};
lastPing = System.currentTimeMillis();
ScheduledExecutorService monitor = Executors.newScheduledThreadPool(1);
monitor.scheduleWithFixedDelay(autokiller, autokillPingIntervalMs, autokillPingIntervalMs, TimeUnit.MILLISECONDS);
}

State getState() {
return lifecycle.getState();
}

Thread getShutdownHook() {
return shutdownHook;
}

public static ProcessEntryPoint createForArguments(String[] args) {
Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args);
return new ProcessEntryPoint(props, new SystemExit());
}
}

+ 53
- 0
server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java View File

@@ -0,0 +1,53 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.joran.JoranConfigurator;
import ch.qos.logback.core.joran.spi.JoranException;
import ch.qos.logback.core.util.StatusPrinter;
import org.slf4j.LoggerFactory;

public class ProcessLogging {

private static final String PATH_LOGS_PROPERTY = "sonar.path.logs";

public void configure(Props props, String logbackXmlResource) {
LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory();
try {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(context);
context.reset();
context.putProperty(PATH_LOGS_PROPERTY, props.nonNullValue(PATH_LOGS_PROPERTY));
doConfigure(configurator, logbackXmlResource);
} catch (JoranException ignored) {
// StatusPrinter will handle this
}
StatusPrinter.printInCaseOfErrorsOrWarnings(context);

}

/**
* Extracted only for unit testing
*/
void doConfigure(JoranConfigurator configurator, String logbackXmlResource) throws JoranException {
configurator.doConfigure(getClass().getResource(logbackXmlResource));
}
}

+ 28
- 0
server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java View File

@@ -0,0 +1,28 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

public interface ProcessMXBean extends Terminable {

boolean isReady();

void ping();

}

+ 77
- 0
server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java View File

@@ -0,0 +1,77 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.apache.commons.io.IOUtils;
import org.slf4j.LoggerFactory;

import javax.annotation.Nullable;

public class ProcessUtils {

private ProcessUtils() {
// only static stuff
}

/**
* Do not abuse to this method. It uses exceptions to get status.
* @return false if process is null or terminated, else true.
*/
public static boolean isAlive(@Nullable Process process) {
boolean alive = false;
if (process != null) {
try {
process.exitValue();
} catch (IllegalThreadStateException ignored) {
alive = true;
}
}
return alive;
}

/**
* Destroys process (equivalent to kill -9) if alive
* @return true if the process was destroyed, false if process is null or already destroyed.
*/
public static boolean destroyQuietly(@Nullable Process process) {
boolean destroyed = false;
if (isAlive(process)) {
try {
process.destroy();
while (isAlive(process)) {
// destroy() sends the signal, it does not wait for the process to be down
Thread.sleep(100L);
}
destroyed = true;
} catch (Exception e) {
LoggerFactory.getLogger(ProcessUtils.class).error("Fail to destroy " + process);
}
}
return destroyed;
}

public static void closeStreams(@Nullable Process process) {
if (process != null) {
IOUtils.closeQuietly(process.getInputStream());
IOUtils.closeQuietly(process.getOutputStream());
IOUtils.closeQuietly(process.getErrorStream());
}
}
}

+ 120
- 0
server/sonar-process/src/main/java/org/sonar/process/Props.java View File

@@ -0,0 +1,120 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.apache.commons.lang.StringUtils;

import javax.annotation.CheckForNull;
import javax.annotation.Nullable;

import java.io.File;
import java.util.Properties;

public class Props {

private final Properties properties;
private final Encryption encryption;

public Props(Properties props) {
this.properties = props;
this.encryption = new Encryption(props.getProperty(AesCipher.ENCRYPTION_SECRET_KEY_PATH));
}

public boolean contains(String key) {
return properties.containsKey(key);
}

@CheckForNull
public String value(String key) {
String value = properties.getProperty(key);
if (value != null && encryption.isEncrypted(value)) {
value = encryption.decrypt(value);
}
return value;
}

public String nonNullValue(String key) {
String value = value(key);
if (value == null) {
throw new IllegalArgumentException("Missing property: " + key);
}
return value;
}

@CheckForNull
public String value(String key, @Nullable String defaultValue) {
String s = value(key);
return s == null ? defaultValue : s;
}

public boolean valueAsBoolean(String key) {
String s = value(key);
return s != null && Boolean.parseBoolean(s);
}

public boolean valueAsBoolean(String key, boolean defaultValue) {
String s = value(key);
return s != null ? Boolean.parseBoolean(s) : defaultValue;
}

public File nonNullValueAsFile(String key) {
String s = value(key);
if (s == null) {
throw new IllegalArgumentException("Property " + key + " is missing");
}
return new File(s);
}

@CheckForNull
public Integer valueAsInt(String key) {
String s = value(key);
if (s != null && !"".equals(s)) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
throw new IllegalStateException("Value of property " + key + " is not an integer: " + s, e);
}
}
return null;
}

public int valueAsInt(String key, int defaultValue) {
Integer i = valueAsInt(key);
return i == null ? defaultValue : i;
}

public Properties rawProperties() {
return properties;
}

public Props set(String key, @Nullable String value) {
if (value != null) {
properties.setProperty(key, value);
}
return this;
}

public void setDefault(String key, String value) {
String s = properties.getProperty(key);
if (StringUtils.isBlank(s)) {
properties.setProperty(key, value);
}
}
}

+ 26
- 0
server/sonar-process/src/main/java/org/sonar/process/State.java View File

@@ -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

}

+ 57
- 0
server/sonar-process/src/main/java/org/sonar/process/StopperThread.java View File

@@ -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();
}
}
}

+ 52
- 0
server/sonar-process/src/main/java/org/sonar/process/SystemExit.java View File

@@ -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);
}
}

+ 28
- 0
server/sonar-process/src/main/java/org/sonar/process/Terminable.java View File

@@ -0,0 +1,28 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

/**
* This term "terminate" is used in order to not conflict with {@link Thread#stop()}.
*/
public interface Terminable {

void terminate();
}

+ 23
- 0
server/sonar-process/src/main/java/org/sonar/process/package-info.java View File

@@ -0,0 +1,23 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
@ParametersAreNonnullByDefault
package org.sonar.process;

import javax.annotation.ParametersAreNonnullByDefault;

+ 185
- 0
server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java View File

@@ -0,0 +1,185 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

package org.sonar.process;

import com.google.common.io.Resources;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang.StringUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

import javax.crypto.BadPaddingException;
import java.io.File;
import java.security.InvalidKeyException;
import java.security.Key;

import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;


public class AesCipherTest {

@Rule
public ExpectedException thrown = ExpectedException.none();

@Test
public void generateRandomSecretKey() {
AesCipher cipher = new AesCipher(null);

String key = cipher.generateRandomSecretKey();

assertThat(StringUtils.isNotBlank(key)).isTrue();
assertThat(Base64.isArrayByteBase64(key.getBytes())).isTrue();
}

@Test
public void encrypt() throws Exception {
AesCipher cipher = new AesCipher(pathToSecretKey());

String encryptedText = cipher.encrypt("this is a secret");

assertThat(StringUtils.isNotBlank(encryptedText)).isTrue();
assertThat(Base64.isArrayByteBase64(encryptedText.getBytes())).isTrue();
}

@Test
public void encrypt_bad_key() throws Exception {
thrown.expect(RuntimeException.class);
thrown.expectMessage("Invalid AES key");

AesCipher cipher = new AesCipher(getPath("bad_secret_key.txt"));

cipher.encrypt("this is a secret");
}

@Test
public void decrypt() throws Exception {
AesCipher cipher = new AesCipher(pathToSecretKey());

// the following value has been encrypted with the key /org/sonar/api/config/AesCipherTest/aes_secret_key.txt
String clearText = cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");

assertThat(clearText).isEqualTo("this is a secret");
}

@Test
public void decrypt_bad_key() throws Exception {
AesCipher cipher = new AesCipher(getPath("bad_secret_key.txt"));

try {
cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
fail();

} catch (RuntimeException e) {
assertThat(e.getCause()).isInstanceOf(InvalidKeyException.class);
}
}

@Test
public void decrypt_other_key() throws Exception {
AesCipher cipher = new AesCipher(getPath("other_secret_key.txt"));

try {
// text encrypted with another key
cipher.decrypt("9mx5Zq4JVyjeChTcVjEide4kWCwusFl7P2dSVXtg9IY=");
fail();

} catch (RuntimeException e) {
assertThat(e.getCause()).isInstanceOf(BadPaddingException.class);
}
}

@Test
public void encryptThenDecrypt() throws Exception {
AesCipher cipher = new AesCipher(pathToSecretKey());

assertThat(cipher.decrypt(cipher.encrypt("foo"))).isEqualTo("foo");
}

@Test
public void testDefaultPathToSecretKey() {
AesCipher cipher = new AesCipher(null);

String path = cipher.getPathToSecretKey();

assertThat(StringUtils.isNotBlank(path)).isTrue();
assertThat(new File(path).getName()).isEqualTo("sonar-secret.txt");
}

@Test
public void loadSecretKeyFromFile() throws Exception {
AesCipher cipher = new AesCipher(null);
Key secretKey = cipher.loadSecretFileFromFile(pathToSecretKey());
assertThat(secretKey.getAlgorithm()).isEqualTo("AES");
assertThat(secretKey.getEncoded().length).isGreaterThan(10);
}

@Test
public void loadSecretKeyFromFile_trim_content() throws Exception {
String path = getPath("non_trimmed_secret_key.txt");
AesCipher cipher = new AesCipher(null);

Key secretKey = cipher.loadSecretFileFromFile(path);

assertThat(secretKey.getAlgorithm()).isEqualTo("AES");
assertThat(secretKey.getEncoded().length).isGreaterThan(10);
}

@Test
public void loadSecretKeyFromFile_file_does_not_exist() throws Exception {
thrown.expect(IllegalStateException.class);

AesCipher cipher = new AesCipher(null);
cipher.loadSecretFileFromFile("/file/does/not/exist");
}

@Test
public void loadSecretKeyFromFile_no_property() throws Exception {
thrown.expect(IllegalStateException.class);

AesCipher cipher = new AesCipher(null);
cipher.loadSecretFileFromFile(null);
}

@Test
public void hasSecretKey() throws Exception {
AesCipher cipher = new AesCipher(pathToSecretKey());

assertThat(cipher.hasSecretKey()).isTrue();
}

@Test
public void doesNotHaveSecretKey() throws Exception {
AesCipher cipher = new AesCipher("/my/twitter/id/is/SimonBrandhof");

assertThat(cipher.hasSecretKey()).isFalse();
}

private static String getPath(String file) {
return Resources.getResource(AesCipherTest.class, "AesCipherTest/" + file).getPath();
}

private static String pathToSecretKey() throws Exception {
return getPath("aes_secret_key.txt");
}

}

+ 59
- 0
server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java View File

@@ -0,0 +1,59 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.apache.commons.io.FileUtils;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.rules.TemporaryFolder;

import java.io.File;
import java.io.IOException;
import java.net.ServerSocket;

public abstract class BaseProcessTest {

@Rule
public TemporaryFolder temp = new TemporaryFolder();

public static final String DUMMY_OK_APP = "org.sonar.application.DummyOkProcess";

int freePort;
File dummyAppJar;
Process proc;

@Before
public void setup() throws IOException {
ServerSocket socket = new ServerSocket(0);
freePort = socket.getLocalPort();
socket.close();

dummyAppJar = FileUtils.toFile(getClass().getResource("/sonar-dummy-app.jar"));
}

@After
public void tearDown() {
if (proc != null) {
ProcessUtils.destroyQuietly(proc);
}
}

}

+ 95
- 0
server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java View File

@@ -0,0 +1,95 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import com.google.common.collect.Maps;
import org.apache.commons.io.FileUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.File;
import java.util.Map;
import java.util.Properties;

import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;

public class ConfigurationUtilsTest {

@Rule
public TemporaryFolder temp = new TemporaryFolder();

@Test
public void shouldInterpolateVariables() {
Properties input = new Properties();
input.setProperty("hello", "world");
input.setProperty("url", "${env:SONAR_JDBC_URL}");
input.setProperty("do_not_change", "${SONAR_JDBC_URL}");
Map<String, String> variables = Maps.newHashMap();
variables.put("SONAR_JDBC_URL", "jdbc:h2:mem");

Properties output = ConfigurationUtils.interpolateVariables(input, variables);

assertThat(output).hasSize(3);
assertThat(output.getProperty("hello")).isEqualTo("world");
assertThat(output.getProperty("url")).isEqualTo("jdbc:h2:mem");
assertThat(output.getProperty("do_not_change")).isEqualTo("${SONAR_JDBC_URL}");

// input is not changed
assertThat(input).hasSize(3);
assertThat(input.getProperty("hello")).isEqualTo("world");
assertThat(input.getProperty("url")).isEqualTo("${env:SONAR_JDBC_URL}");
assertThat(input.getProperty("do_not_change")).isEqualTo("${SONAR_JDBC_URL}");
}

@Test
public void loadPropsFromCommandLineArgs_missing_argument() throws Exception {
try {
ConfigurationUtils.loadPropsFromCommandLineArgs(new String[0]);
fail();
} catch (IllegalArgumentException e) {
assertThat(e.getMessage()).startsWith("Only a single command-line argument is accepted");
}
}

@Test
public void loadPropsFromCommandLineArgs_load_properties_from_file() throws Exception {
File propsFile = temp.newFile();
FileUtils.write(propsFile, "foo=bar");

Props result = ConfigurationUtils.loadPropsFromCommandLineArgs(new String[] {propsFile.getAbsolutePath()});
assertThat(result.value("foo")).isEqualTo("bar");
assertThat(result.rawProperties()).hasSize(1);
}

@Test
public void loadPropsFromCommandLineArgs_file_does_not_exist() throws Exception {
File propsFile = temp.newFile();
FileUtils.deleteQuietly(propsFile);

try {
ConfigurationUtils.loadPropsFromCommandLineArgs(new String[]{propsFile.getAbsolutePath()});
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage("Could not read properties from file: " + propsFile.getAbsolutePath());
}
}
}

+ 59
- 0
server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java View File

@@ -0,0 +1,59 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/

package org.sonar.process;

import org.junit.Test;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;

public class EncryptionTest {

@Test
public void isEncrypted() {
Encryption encryption = new Encryption(null);
assertThat(encryption.isEncrypted("{aes}ADASDASAD"), is(true));
assertThat(encryption.isEncrypted("{b64}ADASDASAD"), is(true));
assertThat(encryption.isEncrypted("{abc}ADASDASAD"), is(true));

assertThat(encryption.isEncrypted("{}"), is(false));
assertThat(encryption.isEncrypted("{foo"), is(false));
assertThat(encryption.isEncrypted("foo{aes}"), is(false));
}

@Test
public void decrypt() {
Encryption encryption = new Encryption(null);
assertThat(encryption.decrypt("{b64}Zm9v"), is("foo"));
}

@Test
public void decrypt_unknown_algorithm() {
Encryption encryption = new Encryption(null);
assertThat(encryption.decrypt("{xxx}Zm9v"), is("{xxx}Zm9v"));
}

@Test
public void decrypt_uncrypted_text() {
Encryption encryption = new Encryption(null);
assertThat(encryption.decrypt("foo"), is("foo"));
}
}

+ 120
- 0
server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java View File

@@ -0,0 +1,120 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.junit.Test;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.remote.JMXServiceURL;

import java.lang.management.ManagementFactory;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;

import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;

public class JmxUtilsTest {

class MyBean implements ProcessMXBean {


@Override
public void terminate() {

}

@Override
public void ping() {

}

@Override
public boolean isReady() {
return true;
}
}

@Test
public void construct_jmx_objectName() throws Exception {
MyBean mxBean = new MyBean();
ObjectName objectName = JmxUtils.objectName(mxBean.getClass().getSimpleName());
assertThat(objectName).isNotNull();
assertThat(objectName.getDomain()).isEqualTo(JmxUtils.DOMAIN);
assertThat(objectName.getKeyProperty(JmxUtils.NAME_PROPERTY)).isEqualTo(mxBean.getClass().getSimpleName());
}

@Test
public void fail_jmx_objectName() throws Exception {
try {
JmxUtils.objectName(":");
fail();
} catch (Exception e) {
assertThat(e.getMessage()).isEqualTo("Cannot create ObjectName for :");
}
}

@Test
public void testRegisterMBean() throws Exception {
// 0 Get mbServer and create out test MXBean
MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
MyBean mxBean = new MyBean();
ObjectName objectName = JmxUtils.objectName(mxBean.getClass().getSimpleName());

// 1 assert that mxBean gets registered
assertThat(mbeanServer.isRegistered(objectName)).isFalse();
JmxUtils.registerMBean(mxBean, mxBean.getClass().getSimpleName());
assertThat(mbeanServer.isRegistered(objectName)).isTrue();
}

@Test
public void serviceUrl_ipv4() throws Exception {
JMXServiceURL url = JmxUtils.serviceUrl(ip(Inet4Address.class), 1234);
assertThat(url).isNotNull();
assertThat(url.getPort()).isEqualTo(1234);
}

@Test
public void serviceUrl_ipv6() throws Exception {
JMXServiceURL url = JmxUtils.serviceUrl(ip(Inet6Address.class), 1234);
assertThat(url).isNotNull();
assertThat(url.getPort()).isEqualTo(1234);
}

private static InetAddress ip(Class inetAddressClass) throws SocketException {
Enumeration<NetworkInterface> ifaces = NetworkInterface.getNetworkInterfaces();
while (ifaces.hasMoreElements()) {
NetworkInterface iface = ifaces.nextElement();
Enumeration<InetAddress> addresses = iface.getInetAddresses();
while (addresses.hasMoreElements()) {
InetAddress addr = addresses.nextElement();
if (addr.getClass().isAssignableFrom(inetAddressClass)) {
return addr;
}
}
}
throw new IllegalStateException("no ipv4 address");
}
}

+ 42
- 0
server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java View File

@@ -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);
}
}

+ 51
- 0
server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java View File

@@ -0,0 +1,51 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import com.google.common.collect.Iterators;
import org.junit.Test;

import java.net.NetworkInterface;
import java.util.Collections;
import java.util.Enumeration;

import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;

public class LoopbackAddressTest {

@Test
public void get() throws Exception {
assertThat(LoopbackAddress.get()).isNotNull();
assertThat(LoopbackAddress.get().isLoopbackAddress()).isTrue();
assertThat(LoopbackAddress.get().getHostAddress()).isNotNull();
}

@Test
public void fail_to_get_loopback_address() throws Exception {
Enumeration<NetworkInterface> ifaces = Iterators.asEnumeration(Collections.<NetworkInterface>emptyList().iterator());
try {
LoopbackAddress.doGet(ifaces);
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage("Impossible to get a IPv4 loopback address");
}
}
}

+ 102
- 0
server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java View File

@@ -0,0 +1,102 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.fest.assertions.Assertions;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.File;

import static org.fest.assertions.Fail.fail;

public class MinimumViableSystemTest {

@Rule
public TemporaryFolder temp = new TemporaryFolder();

/**
* Verifies that all checks can be verified without error.
* Test environment does not necessarily follows all checks.
*/
@Test
public void check() throws Exception {
MinimumViableSystem mve = new MinimumViableSystem();

try {
mve.check();
// ok
} catch (MessageException e) {
// also ok. All other exceptions are errors.
}
}

@Test
public void checkJavaVersion() throws Exception {
MinimumViableSystem mve = new MinimumViableSystem();

// yes, sources are compiled with a supported Java version!
mve.checkJavaVersion();
mve.checkJavaVersion("1.6");

try {
mve.checkJavaVersion("1.9");
fail();
} catch (MessageException e) {
Assertions.assertThat(e).hasMessage("Supported versions of Java are 1.6, 1.7 and 1.8. Got 1.9.");
}
}

@Test
public void checkJavaOption() throws Exception {
String key = "MinimumViableEnvironmentTest.test.prop";
MinimumViableSystem mve = new MinimumViableSystem()
.setRequiredJavaOption(key, "true");

try {
System.setProperty(key, "false");
mve.checkJavaOptions();
fail();
} catch (MessageException e) {
Assertions.assertThat(e).hasMessage("JVM option '" + key + "' must be set to 'true'. Got 'false'");
}

System.setProperty(key, "true");
mve.checkJavaOptions();
// do not fail
}

@Test
public void checkWritableTempDir() throws Exception {
File dir = temp.newFolder();
MinimumViableSystem mve = new MinimumViableSystem();

mve.checkWritableDir(dir.getAbsolutePath());

dir.delete();
try {
mve.checkWritableDir(dir.getAbsolutePath());
fail();
} catch (IllegalStateException e) {
Assertions.assertThat(e).hasMessage("Temp directory is not writable: " + dir.getAbsolutePath());
}
}
}

+ 61
- 0
server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java View File

@@ -0,0 +1,61 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.junit.Test;

import java.net.ServerSocket;

import static org.fest.assertions.Assertions.assertThat;

public class NetworkUtilsTest {


@Test
public void find_free_port() throws Exception {
int port = NetworkUtils.freePort();
assertThat(port).isGreaterThan(1024);
}

@Test
public void find_multiple_free_port() throws Exception {
int port1 = NetworkUtils.freePort();
int port2 = NetworkUtils.freePort();

assertThat(port1).isGreaterThan(1024);
assertThat(port2).isGreaterThan(1024);

assertThat(port1).isNotSameAs(port2);
}

@Test
public void find_multiple_free_non_adjacent_port() throws Exception {
int port1 = NetworkUtils.freePort();

ServerSocket socket = new ServerSocket(port1 + 1);

int port2 = NetworkUtils.freePort();

assertThat(port1).isGreaterThan(1024);
assertThat(port2).isGreaterThan(1024);

assertThat(port1).isNotSameAs(port2);
}
}

+ 224
- 0
server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java View File

@@ -0,0 +1,224 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.apache.commons.io.FileUtils;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.rules.Timeout;
import org.sonar.process.test.StandardProcess;

import java.io.File;
import java.util.Properties;

import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
import static org.mockito.Mockito.mock;

public class ProcessEntryPointTest {

SystemExit exit = mock(SystemExit.class);

/**
* Safeguard
*/
@Rule
public Timeout timeout = new Timeout(10000);

@Rule
public TemporaryFolder temp = new TemporaryFolder();

@Test
public void load_properties_from_file() throws Exception {
File propsFile = temp.newFile();
FileUtils.write(propsFile, "sonar.foo=bar");

ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(new String[]{propsFile.getAbsolutePath()});
assertThat(entryPoint.getProps().value("sonar.foo")).isEqualTo("bar");
}

@Test
public void test_initial_state() throws Exception {
Props props = new Props(new Properties());
ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);

assertThat(entryPoint.getProps()).isSameAs(props);
assertThat(entryPoint.isReady()).isFalse();
assertThat(entryPoint.getState()).isEqualTo(State.INIT);

// do not fail
entryPoint.ping();
}

@Test
public void fail_to_launch_if_missing_monitor_properties() throws Exception {
Props props = new Props(new Properties());
ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);

StandardProcess process = new StandardProcess();
try {
entryPoint.launch(process);
fail();
} catch (IllegalArgumentException e) {
assertThat(e).hasMessage("Missing property: process.key");
assertThat(process.getState()).isEqualTo(State.INIT);
}
}

@Test
public void fail_to_launch_multiple_times() throws Exception {
Props props = new Props(new Properties());
props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test");
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true");
props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);

entryPoint.launch(new NoopProcess());
try {
entryPoint.launch(new NoopProcess());
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage("Already started");
}
}

@Test
public void launch_then_request_graceful_termination() throws Exception {
Props props = new Props(new Properties());
props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test");
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true");
props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
final StandardProcess process = new StandardProcess();

Thread runner = new Thread() {
@Override
public void run() {
// starts and waits until terminated
entryPoint.launch(process);
}
};
runner.start();

while (process.getState() != State.STARTED) {
Thread.sleep(10L);
}

// requests for termination -> waits until down
// Should terminate before the timeout of 30s
entryPoint.terminate();

assertThat(process.getState()).isEqualTo(State.STOPPED);
}

@Test
public void autokill_if_no_pings() throws Exception {
Props props = new Props(new Properties());
props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test");
props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_INTERVAL, "5");
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_TIMEOUT, "1");
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
final StandardProcess process = new StandardProcess();

entryPoint.launch(process);

assertThat(process.getState()).isEqualTo(State.STOPPED);
}

@Test
public void terminate_if_unexpected_shutdown() throws Exception {
Props props = new Props(new Properties());
props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo");
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true");
props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
final StandardProcess process = new StandardProcess();

Thread runner = new Thread() {
@Override
public void run() {
// starts and waits until terminated
entryPoint.launch(process);
}
};
runner.start();
while (process.getState() != State.STARTED) {
Thread.sleep(10L);
}

// emulate signal to shutdown process
entryPoint.getShutdownHook().start();
while (process.getState() != State.STOPPED) {
Thread.sleep(10L);
}
// exit before test timeout, ok !
}

@Test
public void terminate_if_startup_error() throws Exception {
Props props = new Props(new Properties());
props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo");
props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true");
props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000");
final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit);
final MonitoredProcess process = new StartupErrorProcess();

entryPoint.launch(process);
assertThat(entryPoint.getState()).isEqualTo(State.STOPPED);
}

private static class NoopProcess implements MonitoredProcess {

@Override
public void start() {

}

@Override
public void awaitTermination() {

}

@Override
public void terminate() {

}
}

private static class StartupErrorProcess implements MonitoredProcess {

@Override
public void start() {
throw new IllegalStateException("ERROR");
}

@Override
public void awaitTermination() {

}

@Override
public void terminate() {

}
}
}

+ 28
- 0
server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java View File

@@ -0,0 +1,28 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.junit.Test;

import static org.fest.assertions.Assertions.assertThat;

public class ProcessUtilsTest {

}

+ 135
- 0
server/sonar-process/src/test/java/org/sonar/process/PropsTest.java View File

@@ -0,0 +1,135 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process;

import org.junit.Test;

import java.util.Properties;

import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;

public class PropsTest {

@Test
public void of() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "bar");
Props props = new Props(p);

assertThat(props.value("foo")).isEqualTo("bar");
assertThat(props.value("foo", "default value")).isEqualTo("bar");
assertThat(props.value("unknown")).isNull();
assertThat(props.value("unknown", "default value")).isEqualTo("default value");
}

@Test
public void intOf() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "33");
p.setProperty("blank", "");
Props props = new Props(p);

assertThat(props.valueAsInt("foo")).isEqualTo(33);
assertThat(props.valueAsInt("foo", 44)).isEqualTo(33);
assertThat(props.valueAsInt("blank")).isNull();
assertThat(props.valueAsInt("blank", 55)).isEqualTo(55);
assertThat(props.valueAsInt("unknown")).isNull();
assertThat(props.valueAsInt("unknown", 44)).isEqualTo(44);
}

@Test
public void intOf_not_integer() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "bar");
Props props = new Props(p);

try {
props.valueAsInt("foo");
fail();
} catch (IllegalStateException e) {
assertThat(e).hasMessage("Value of property foo is not an integer: bar");
}
}

@Test
public void booleanOf() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "True");
p.setProperty("bar", "false");
Props props = new Props(p);

assertThat(props.valueAsBoolean("foo")).isTrue();
assertThat(props.valueAsBoolean("bar")).isFalse();
assertThat(props.valueAsBoolean("unknown")).isFalse();
}

@Test
public void booleanOf_default_value() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "true");
p.setProperty("bar", "false");
Props props = new Props(p);

assertThat(props.valueAsBoolean("unset", false)).isFalse();
assertThat(props.valueAsBoolean("unset", true)).isTrue();
assertThat(props.valueAsBoolean("foo", false)).isTrue();
assertThat(props.valueAsBoolean("bar", true)).isFalse();
}

@Test
public void setDefault() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "foo_value");
Props props = new Props(p);
props.setDefault("foo", "foo_def");
props.setDefault("bar", "bar_def");

assertThat(props.value("foo")).isEqualTo("foo_value");
assertThat(props.value("bar")).isEqualTo("bar_def");
assertThat(props.value("other")).isNull();
}

@Test
public void set() throws Exception {
Properties p = new Properties();
p.setProperty("foo", "old_foo");
Props props = new Props(p);
props.set("foo", "new_foo");
props.set("bar", "new_bar");

assertThat(props.value("foo")).isEqualTo("new_foo");
assertThat(props.value("bar")).isEqualTo("new_bar");
}

@Test
public void raw_properties() throws Exception {
Properties p = new Properties();
p.setProperty("encrypted_prop", "{aes}abcde");
p.setProperty("clear_prop", "foo");
Props props = new Props(p);

assertThat(props.rawProperties()).hasSize(2);
// do not decrypt
assertThat(props.rawProperties().get("encrypted_prop")).isEqualTo("{aes}abcde");
assertThat(props.rawProperties().get("clear_prop")).isEqualTo("foo");

}
}

+ 56
- 0
server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java View File

@@ -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);
}
}

+ 116
- 0
server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java View File

@@ -0,0 +1,116 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.test;

import org.apache.commons.io.FileUtils;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.handler.AbstractHandler;
import org.eclipse.jetty.server.handler.ContextHandler;
import org.sonar.process.MonitoredProcess;
import org.sonar.process.ProcessEntryPoint;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.io.File;
import java.io.IOException;

/**
* Http server used for testing (see MonitorTest). It accepts HTTP commands /ping and /kill to hardly exit.
* It also pushes status to temp files, so test can verify what was really done (when server went ready state and
* if it was gracefully terminated)
*/
public class HttpProcess implements MonitoredProcess {

private final Server server;
// temp dir is specific to this process
private final File tempDir = new File(System.getProperty("java.io.tmpdir"));

public HttpProcess(int httpPort) {
server = new Server(httpPort);
}

@Override
public void start() {
writeTimeToFile("startingAt");
ContextHandler context = new ContextHandler();
context.setContextPath("/");
context.setClassLoader(Thread.currentThread().getContextClassLoader());
server.setHandler(context);
context.setHandler(new AbstractHandler() {
@Override
public void handle(String target, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException {
if ("/ping".equals(target)) {
request.setHandled(true);
httpServletResponse.getWriter().print("ping");
} else if ("/kill".equals(target)) {
writeTimeToFile("killedAt");
System.exit(0);
}
}
});
try {
server.start();
while (!server.isStarted()) {
Thread.sleep(100L);
}
writeTimeToFile("readyAt");

} catch (Exception e) {
throw new IllegalStateException("Fail to start Jetty", e);
}
}

@Override
public void awaitTermination() {
try {
server.join();
} catch (InterruptedException ignore) {

}
}

@Override
public void terminate() {
try {
if (!server.isStopped()) {
server.stop();
writeTimeToFile("terminatedAt");
}
} catch (Exception e) {
throw new IllegalStateException("Fail to stop Jetty", e);
}
}

private void writeTimeToFile(String filename) {
try {
FileUtils.write(new File(tempDir, filename), String.valueOf(System.currentTimeMillis()));
} catch (IOException e) {
throw new IllegalStateException(e);
}
}

public static void main(String[] args) {
ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
entryPoint.launch(new HttpProcess(entryPoint.getProps().valueAsInt("httpPort")));
}
}

+ 81
- 0
server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java View File

@@ -0,0 +1,81 @@
/*
* SonarQube, open source software quality management tool.
* Copyright (C) 2008-2014 SonarSource
* mailto:contact AT sonarsource DOT com
*
* SonarQube is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* SonarQube is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package org.sonar.process.test;

import org.sonar.process.MonitoredProcess;
import org.sonar.process.ProcessEntryPoint;
import org.sonar.process.State;

public class StandardProcess implements MonitoredProcess {

private State state = State.INIT;

private final Thread daemon = new Thread() {
@Override
public void run() {
try {
while (true) {
Thread.sleep(100L);
}
} catch (InterruptedException e) {
// return
}
}
};

/**
* Blocks until started()
*/
@Override
public void start() {
state = State.STARTING;
daemon.start();
state = State.STARTED;
}

@Override
public void awaitTermination() {
try {
daemon.join();
} catch (InterruptedException e) {
// interrupted by call to terminate()
}
}

/**
* Blocks until stopped
*/
@Override
public void terminate() {
state = State.STOPPING;
daemon.interrupt();
state = State.STOPPED;
}

public State getState() {
return state;
}

public static void main(String[] args) {
ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
entryPoint.launch(new StandardProcess());
System.exit(0);
}
}

+ 1
- 0
server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt View File

@@ -0,0 +1 @@
0PZz+G+f8mjr3sPn4+AhHg==

+ 1
- 0
server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt View File

@@ -0,0 +1 @@
badbadbad==

+ 3
- 0
server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt View File

@@ -0,0 +1,3 @@

0PZz+G+f8mjr3sPn4+AhHg==


+ 1
- 0
server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt View File

@@ -0,0 +1 @@
IBxEUxZ41c8XTxyaah1Qlg==

+ 1
- 0
server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml View File

@@ -0,0 +1 @@
<configuration/>

+ 212
- 0
server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties View File

@@ -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

+ 3
- 0
server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties View File

@@ -0,0 +1,3 @@
hello: world
foo=bar
java.io.tmpdir=/should/be/overridden

BIN
server/sonar-process/src/test/resources/sonar-dummy-app.jar View File


+ 19
- 0
server/sonar-process/test-jar-with-dependencies.xml View File

@@ -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>

+ 36
- 53
server/sonar-search/src/main/java/org/sonar/search/SearchServer.java View File

@@ -21,15 +21,14 @@ package org.sonar.search;

import org.apache.commons.lang.StringUtils;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus;
import org.elasticsearch.common.annotations.VisibleForTesting;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.node.Node;
import org.elasticsearch.node.NodeBuilder;
import org.slf4j.LoggerFactory;
import org.sonar.process.ConfigurationUtils;
import org.sonar.process.MinimumViableSystem;
import org.sonar.process.MonitoredProcess;
import org.sonar.process.ProcessEntryPoint;
import org.sonar.process.ProcessLogging;
import org.sonar.process.Props;
import org.sonar.search.script.ListUpdate;
@@ -40,7 +39,7 @@ import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

public class SearchServer extends MonitoredProcess {
public class SearchServer implements MonitoredProcess {

public static final String SONAR_NODE_NAME = "sonar.node.name";
public static final String ES_PORT_PROPERTY = "sonar.search.port";
@@ -55,27 +54,13 @@ public class SearchServer extends MonitoredProcess {
private static final Integer MINIMUM_INDEX_REPLICATION = 1;

private final Set<String> nodes = new HashSet<String>();
private final boolean isBlocking;

private Node node;
private final Props props;
private final Object lock = new Object();

@VisibleForTesting
public SearchServer(final Props props, boolean monitored, boolean blocking) {
super(props, monitored);

this.isBlocking = blocking;
new MinimumViableSystem().check();

String esNodesInets = props.value(ES_CLUSTER_INET);
if (StringUtils.isNotEmpty(esNodesInets)) {
Collections.addAll(nodes, esNodesInets.split(","));
}
}
private Node node;

public SearchServer(Props props) {
super(props);
this.isBlocking = true;
this.props = props;
new MinimumViableSystem().check();

String esNodesInets = props.value(ES_CLUSTER_INET);
@@ -85,18 +70,8 @@ public class SearchServer extends MonitoredProcess {
}

@Override
protected boolean doIsReady() {
return node.client().admin().cluster().prepareHealth()
.setWaitForYellowStatus()
.setTimeout(TimeValue.timeValueSeconds(3L))
.get()
.getStatus() != ClusterHealthStatus.RED;
}

@Override
protected void doStart() {
public void start() {
synchronized (lock) {

Integer port = props.valueAsInt(ES_PORT_PROPERTY);
String clusterName = props.value(ES_CLUSTER_PROPERTY);

@@ -169,17 +144,25 @@ public class SearchServer extends MonitoredProcess {
.addMapping("_default_", "{\"dynamic\": \"strict\"}")
.get();
}
}

if (isBlocking) {
while (node != null && !node.isClosed()) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
// Ignore
}
boolean isReady() {
return node.client().admin().cluster().prepareHealth()
.setWaitForYellowStatus()
.setTimeout(TimeValue.timeValueSeconds(3L))
.get()
.getStatus() != ClusterHealthStatus.RED;
}

@Override
public void awaitTermination() {
while (node != null && !node.isClosed()) {
try {
Thread.sleep(200L);
} catch (InterruptedException e) {
// Ignore
}
}

}

private void initAnalysis(ImmutableSettings.Builder esSettings) {
@@ -188,40 +171,40 @@ public class SearchServer extends MonitoredProcess {
// Disallow dynamic mapping (too expensive)
.put("index.mapper.dynamic", false)

// Sortable text analyzer
// Sortable text analyzer
.put("index.analysis.analyzer.sortable.type", "custom")
.put("index.analysis.analyzer.sortable.tokenizer", "keyword")
.putArray("index.analysis.analyzer.sortable.filter", "trim", "lowercase", "truncate")

// Edge NGram index-analyzer
// Edge NGram index-analyzer
.put("index.analysis.analyzer.index_grams.type", "custom")
.put("index.analysis.analyzer.index_grams.tokenizer", "whitespace")
.putArray("index.analysis.analyzer.index_grams.filter", "trim", "lowercase", "gram_filter")

// Edge NGram search-analyzer
// Edge NGram search-analyzer
.put("index.analysis.analyzer.search_grams.type", "custom")
.put("index.analysis.analyzer.search_grams.tokenizer", "whitespace")
.putArray("index.analysis.analyzer.search_grams.filter", "trim", "lowercase")

// Word index-analyzer
// Word index-analyzer
.put("index.analysis.analyzer.index_words.type", "custom")
.put("index.analysis.analyzer.index_words.tokenizer", "standard")
.putArray("index.analysis.analyzer.index_words.filter",
"standard", "word_filter", "lowercase", "stop", "asciifolding", "porter_stem")

// Word search-analyzer
// Word search-analyzer
.put("index.analysis.analyzer.search_words.type", "custom")
.put("index.analysis.analyzer.search_words.tokenizer", "standard")
.putArray("index.analysis.analyzer.search_words.filter",
"standard", "lowercase", "stop", "asciifolding", "porter_stem")

// Edge NGram filter
// Edge NGram filter
.put("index.analysis.filter.gram_filter.type", "edgeNGram")
.put("index.analysis.filter.gram_filter.min_gram", 2)
.put("index.analysis.filter.gram_filter.max_gram", 15)
.putArray("index.analysis.filter.gram_filter.token_chars", "letter", "digit", "punctuation", "symbol")

// Word filter
// Word filter
.put("index.analysis.filter.word_filter.type", "word_delimiter")
.put("index.analysis.filter.word_filter.generate_word_parts", true)
.put("index.analysis.filter.word_filter.catenate_words", true)
@@ -232,7 +215,7 @@ public class SearchServer extends MonitoredProcess {
.put("index.analysis.filter.word_filter.split_on_numerics", true)
.put("index.analysis.filter.word_filter.stem_english_possessive", true)

// Path Analyzer
// Path Analyzer
.put("index.analysis.analyzer.path_analyzer.type", "custom")
.put("index.analysis.analyzer.path_analyzer.tokenizer", "path_hierarchy");

@@ -267,18 +250,18 @@ public class SearchServer extends MonitoredProcess {
}

@Override
protected void doTerminate() {
public void terminate() {
synchronized (lock) {
if (node != null && !node.isClosed()) {
if (!node.isClosed()) {
node.close();
node = null;
}
}
}

public static void main(String... args) {
Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args);
new ProcessLogging().configure(props, "/org/sonar/search/logback.xml");
new SearchServer(props).start();
ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
new ProcessLogging().configure(entryPoint.getProps(), "/org/sonar/search/logback.xml");
SearchServer searchServer = new SearchServer(entryPoint.getProps());
entryPoint.launch(searchServer);
}
}

+ 3
- 3
server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java View File

@@ -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());

+ 48
- 31
server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java View File

@@ -19,22 +19,32 @@
*/
package org.sonar.server.app;

import com.google.common.base.Throwables;
import com.google.common.util.concurrent.Uninterruptibles;
import org.apache.catalina.Container;
import org.apache.catalina.Executor;
import org.apache.catalina.Lifecycle;
import org.apache.catalina.LifecycleEvent;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.LifecycleState;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.startup.Tomcat;
import org.apache.commons.io.FileUtils;
import org.slf4j.LoggerFactory;
import org.sonar.process.ProcessUtils;
import org.sonar.process.Props;
import org.sonar.process.Terminable;

import java.io.File;
import java.util.concurrent.TimeUnit;

class EmbeddedTomcat implements Terminable {
class EmbeddedTomcat {

private final Props props;
private Tomcat tomcat = null;
private Thread hook = null;
private boolean ready = false;
private volatile StandardContext webappContext;

EmbeddedTomcat(Props props) {
this.props = props;
@@ -62,45 +72,52 @@ class EmbeddedTomcat implements Terminable {
tomcat.getHost().setDeployOnStartup(true);
Logging.configure(tomcat, props);
Connectors.configure(tomcat, props);
StandardContext webappContext = Webapp.configure(tomcat, props);
ProcessUtils.addSelfShutdownHook(this);
webappContext = Webapp.configure(tomcat, props);
tomcat.start();
waitForWebappReady();

if (webappContext.getState().isAvailable()) {
ready = true;
tomcat.getServer().await();
}
} catch (Exception e) {
throw new IllegalStateException("Fail to start web server", e);
} finally {
// Failed to start or received a shutdown command (should never occur as shutdown port is disabled)
terminate();
Throwables.propagate(e);
}
}

private File tomcatBasedir() {
return new File(props.value("sonar.path.temp"), "tc");
private void waitForWebappReady() {
while (true) {
switch (webappContext.getState()) {
case NEW:
case INITIALIZING:
case INITIALIZED:
case STARTING_PREP:
case STARTING:
Uninterruptibles.sleepUninterruptibly(300L, TimeUnit.MILLISECONDS);
break;
case STARTED:
// ok
return;
default:
// problem, stopped or failed
throw new IllegalStateException("YYY Webapp did not start");
}
}
}

boolean isReady() {
return ready && tomcat != null;
private File tomcatBasedir() {
return new File(props.value("sonar.path.temp"), "tc");
}

@Override
public void terminate() {
if (tomcat != null) {
synchronized (tomcat) {
if (tomcat.getServer().getState().isAvailable()) {
try {
tomcat.stop();
tomcat.destroy();
} catch (Exception e) {
LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to stop web service", e);
}
}
void terminate() {
if (tomcat.getServer().getState().isAvailable()) {
try {
tomcat.stop();
tomcat.destroy();
} catch (Exception e) {
LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to stop web server", e);
}
}
ready = false;
FileUtils.deleteQuietly(tomcatBasedir());
}

void awaitTermination() {
tomcat.getServer().await();
}
}

+ 11
- 18
server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java View File

@@ -19,18 +19,16 @@
*/
package org.sonar.server.app;

import org.slf4j.LoggerFactory;
import org.sonar.process.ConfigurationUtils;
import org.sonar.process.MinimumViableSystem;
import org.sonar.process.MonitoredProcess;
import org.sonar.process.ProcessEntryPoint;
import org.sonar.process.Props;

public class WebServer extends MonitoredProcess {
public class WebServer implements MonitoredProcess {

private final EmbeddedTomcat tomcat;

WebServer(Props props) throws Exception {
super(props);
new MinimumViableSystem()
.setRequiredJavaOption("file.encoding", "UTF-8")
.check();
@@ -38,32 +36,27 @@ public class WebServer extends MonitoredProcess {
}

@Override
protected void doStart() {
try {
tomcat.start();
} catch (Exception e) {
LoggerFactory.getLogger(getClass()).error("TC error", e);
} finally {
terminate();
}
public void start() {
tomcat.start();
}

@Override
protected void doTerminate() {
public void terminate() {
tomcat.terminate();
}

@Override
protected boolean doIsReady() {
return tomcat.isReady();
public void awaitTermination() {
tomcat.awaitTermination();
}

/**
* Can't be started as is. Needs to be bootstrapped by sonar-application
*/
public static void main(String[] args) throws Exception {
Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args);
Logging.init(props);
new WebServer(props).start();
ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args);
Logging.init(entryPoint.getProps());
WebServer server = new WebServer(entryPoint.getProps());
entryPoint.launch(server);
}
}

+ 0
- 1
server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java View File

@@ -64,7 +64,6 @@ class Webapp {
String key = entry.getKey().toString();
context.addParameter(key, entry.getValue().toString());
}

return context;

} catch (Exception e) {

+ 3
- 3
server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java View File

@@ -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);
}
}


+ 5
- 3
server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java View File

@@ -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());

+ 19
- 19
server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java View File

@@ -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();

+ 1
- 2
server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java View File

@@ -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();
}

+ 2
- 8
server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java View File

@@ -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");

+ 5
- 0
sonar-application/pom.xml View File

@@ -25,6 +25,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>

+ 59
- 107
sonar-application/src/main/java/org/sonar/application/App.java View File

@@ -21,19 +21,18 @@ package org.sonar.application;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.process.JmxUtils;
import org.sonar.process.MinimumViableSystem;
import org.sonar.process.Monitor;
import org.sonar.process.ProcessLogging;
import org.sonar.process.ProcessMXBean;
import org.sonar.process.ProcessUtils;
import org.sonar.process.ProcessWrapper;
import org.sonar.process.Props;
import org.sonar.search.SearchServer;
import org.sonar.process.State;
import org.sonar.process.monitor.JavaCommand;
import org.sonar.process.monitor.Monitor;

import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
@@ -41,135 +40,88 @@ import java.util.Properties;
*/
public class App implements ProcessMXBean {

private Monitor monitor = new Monitor();
private ProcessWrapper elasticsearch;
private ProcessWrapper server;
private boolean success = false;
private final Monitor monitor;

public App() {
JmxUtils.registerMBean(this, "SonarQube");
ProcessUtils.addSelfShutdownHook(this);
this(Monitor.create());
}

public void start(Props props) throws InterruptedException {
try {
Logger logger = LoggerFactory.getLogger(getClass());
App(Monitor monitor) {
this.monitor = monitor;
JmxUtils.registerMBean(this, "SonarQube");
}

monitor.start();
public void start(Props props) {
monitor.start(createCommands(props));
monitor.awaitTermination();
}

File homeDir = props.nonNullValueAsFile("sonar.path.home");
File tempDir = props.nonNullValueAsFile("sonar.path.temp");
elasticsearch = new ProcessWrapper(JmxUtils.SEARCH_SERVER_NAME);
elasticsearch
private List<JavaCommand> createCommands(Props props) {
List<JavaCommand> commands = new ArrayList<JavaCommand>();
File homeDir = props.nonNullValueAsFile("sonar.path.home");
File tempDir = props.nonNullValueAsFile("sonar.path.temp");
JavaCommand elasticsearch = new JavaCommand(JmxUtils.SEARCH_SERVER_NAME);
elasticsearch
.setWorkDir(homeDir)
.setJmxPort(props.valueAsInt(DefaultSettings.SEARCH_JMX_PORT))
.addJavaOptions(props.value(DefaultSettings.SEARCH_JAVA_OPTS))
.setTempDir(tempDir.getAbsoluteFile())
.setClassName("org.sonar.search.SearchServer")
.setArguments(props.rawProperties())
.addClasspath("./lib/common/*")
.addClasspath("./lib/search/*");
commands.add(elasticsearch);

// do not yet start SQ in cluster mode. See SONAR-5483 & SONAR-5391
if (StringUtils.isEmpty(props.value(DefaultSettings.CLUSTER_MASTER))) {
JavaCommand webServer = new JavaCommand(JmxUtils.WEB_SERVER_NAME)
.setWorkDir(homeDir)
.setJmxPort(props.valueAsInt(DefaultSettings.SEARCH_JMX_PORT))
.addJavaOpts(props.value(DefaultSettings.SEARCH_JAVA_OPTS))
.setTempDirectory(tempDir.getAbsoluteFile())
.setClassName("org.sonar.search.SearchServer")
.addProperties(props.rawProperties())
.setJmxPort(props.valueAsInt(DefaultSettings.WEB_JMX_PORT))
.addJavaOptions(props.nonNullValue(DefaultSettings.WEB_JAVA_OPTS))
.setTempDir(tempDir.getAbsoluteFile())
// required for logback tomcat valve
.setEnvVariable("sonar.path.logs", props.nonNullValue("sonar.path.logs"))
.setClassName("org.sonar.server.app.WebServer")
.setArguments(props.rawProperties())
.addClasspath("./lib/common/*")
.addClasspath("./lib/search/*");
if (elasticsearch.execute()) {
monitor.monitor(elasticsearch);
if (elasticsearch.waitForReady()) {
logger.info("search server is up");

// do not yet start SQ in cluster mode. See SONAR-5483 & SONAR-5391
if (StringUtils.isEmpty(props.value(DefaultSettings.CLUSTER_MASTER))) {
server = new ProcessWrapper(JmxUtils.WEB_SERVER_NAME)
.setWorkDir(homeDir)
.setJmxPort(props.valueAsInt(DefaultSettings.WEB_JMX_PORT))
.addJavaOpts(props.nonNullValue(DefaultSettings.WEB_JAVA_OPTS))
.setTempDirectory(tempDir.getAbsoluteFile())
// required for logback tomcat valve
.setLogDir(props.nonNullValueAsFile("sonar.path.logs"))
.setClassName("org.sonar.server.app.WebServer")
.addProperties(props.rawProperties())
.addClasspath("./lib/common/*")
.addClasspath("./lib/server/*");
String driverPath = props.value(JdbcSettings.PROPERTY_DRIVER_PATH);
if (driverPath != null) {
server.addClasspath(driverPath);
}
if (server.execute()) {
monitor.monitor(server);
if (server.waitForReady()) {
success = true;
logger.info("web server is up");
}
}
} else {
success = true;
}
}
.addClasspath("./lib/server/*");
String driverPath = props.value(JdbcSettings.PROPERTY_DRIVER_PATH);
if (driverPath != null) {
webServer.addClasspath(driverPath);
}
} finally {
monitor.join();
terminate();
commands.add(webServer);
}
return commands;
}

static String starPath(File homeDir, String relativePath) {
File dir = new File(homeDir, relativePath);
return FilenameUtils.concat(dir.getAbsolutePath(), "*");
@Override
public void terminate() {
monitor.stop();
}

@Override
public boolean isReady() {
return monitor.isAlive();
return monitor.getState() == State.STARTED;
}

@Override
public long ping() {
return System.currentTimeMillis();
}
public void ping() {

@Override
public void terminate() {
if (monitor != null && monitor.isAlive()) {
monitor.terminate();
monitor = null;
}
if (server != null) {
server.terminate();
server = null;
}
if (elasticsearch != null) {
elasticsearch.terminate();
elasticsearch = null;
}
}

private boolean isSuccess() {
return success;
static String starPath(File homeDir, String relativePath) {
File dir = new File(homeDir, relativePath);
return FilenameUtils.concat(dir.getAbsolutePath(), "*");
}

public static void main(String[] args) {
public static void main(String[] args) throws Exception {
new MinimumViableSystem().check();
CommandLineParser cli = new CommandLineParser();
Properties rawProperties = cli.parseArguments(args);
Props props;

try {
props = new PropsBuilder(rawProperties, new JdbcSettings()).build();
new ProcessLogging().configure(props, "/org/sonar/application/logback.xml");
} catch (Exception e) {
throw new IllegalStateException(e);
}
Props props = new PropsBuilder(rawProperties, new JdbcSettings()).build();
new ProcessLogging().configure(props, "/org/sonar/application/logback.xml");

App app = new App();
ProcessUtils.addSelfShutdownHook(app);
try {
// start and wait for shutdown command
if (props.contains(SearchServer.ES_CLUSTER_INET)) {
LoggerFactory.getLogger(App.class).info("SonarQube slave configured to join SonarQube master : {}", props.value(SearchServer.ES_CLUSTER_INET));
}
app.start(props);
} catch (InterruptedException e) {
LoggerFactory.getLogger(App.class).info("interrupted");
} finally {
LoggerFactory.getLogger(App.class).info("stopped");
System.exit(app.isSuccess() ? 0 : 1);
}
app.start(props);
}
}

Loading…
Cancel
Save