diff options
author | Simon Brandhof <simon.brandhof@sonarsource.com> | 2014-09-12 15:56:49 +0200 |
---|---|---|
committer | Simon Brandhof <simon.brandhof@sonarsource.com> | 2014-09-12 15:56:49 +0200 |
commit | e5aea553f53345a490c3d7a0dd89e80358f19e5c (patch) | |
tree | b001f21abcb475377a533b1f7e16d2bfa9675dfc | |
parent | 0ccbeceffbac2071eccca12f807dde9b2934e3eb (diff) | |
parent | 3166381a9020db85e45bfef8ebea9f841736af69 (diff) | |
download | sonarqube-e5aea553f53345a490c3d7a0dd89e80358f19e5c.tar.gz sonarqube-e5aea553f53345a490c3d7a0dd89e80358f19e5c.zip |
Merge remote-tracking branch 'origin/branch-4.5'
Conflicts:
server/process/pom.xml
server/process/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java
server/sonar-process-monitor/pom.xml
server/sonar-process/pom.xml
103 files changed, 3520 insertions, 1545 deletions
@@ -1,6 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.codehaus.sonar</groupId> @@ -1329,7 +1328,7 @@ </goals> </pluginExecutionFilter> <action> - <ignore/> + <ignore /> </action> </pluginExecution> <pluginExecution> @@ -1342,7 +1341,7 @@ </goals> </pluginExecutionFilter> <action> - <ignore/> + <ignore /> </action> </pluginExecution> <pluginExecution> @@ -1356,7 +1355,7 @@ </goals> </pluginExecutionFilter> <action> - <ignore/> + <ignore /> </action> </pluginExecution> <pluginExecution> @@ -1369,7 +1368,7 @@ </goals> </pluginExecutionFilter> <action> - <ignore/> + <ignore /> </action> </pluginExecution> </pluginExecutions> diff --git a/server/pom.xml b/server/pom.xml index c32d9042576..7a5c9de19d8 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -11,7 +11,8 @@ <name>SonarQube :: Server :: Parent</name> <modules> - <module>process</module> + <module>sonar-process</module> + <module>sonar-process-monitor</module> <module>sonar-search</module> <module>sonar-server</module> <module>sonar-web</module> diff --git a/server/process/pom.xml b/server/process/pom.xml deleted file mode 100644 index bd06cb24b37..00000000000 --- a/server/process/pom.xml +++ /dev/null @@ -1,21 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> - <parent> - <groupId>org.codehaus.sonar</groupId> - <artifactId>server</artifactId> - <version>5.0-SNAPSHOT</version> - <relativePath>../</relativePath> - </parent> - <modelVersion>4.0.0</modelVersion> - - <artifactId>process</artifactId> - <packaging>pom</packaging> - <name>SonarQube :: Process :: Parent</name> - - <modules> - <module>sonar-dummy-app</module> - <module>sonar-process</module> - </modules> -</project> diff --git a/server/process/sonar-dummy-app/src/main/java/org/sonar/application/DummyOkProcess.java b/server/process/sonar-dummy-app/src/main/java/org/sonar/application/DummyOkProcess.java deleted file mode 100644 index b25dfccc236..00000000000 --- a/server/process/sonar-dummy-app/src/main/java/org/sonar/application/DummyOkProcess.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.application; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.sonar.process.MonitoredProcess; -import org.sonar.process.Props; - -import java.io.File; -import java.util.Properties; - -public class DummyOkProcess extends MonitoredProcess { - - private static final Logger LOGGER = LoggerFactory.getLogger(DummyOkProcess.class); - - private boolean isReady = false; - private boolean isRunning = true; - private boolean isSuccess = true; - - protected DummyOkProcess(Props props) { - super(props); - try { - File.createTempFile("hello", ".tmp"); - } catch (Exception e) { - LOGGER.error("Could not create file", e); - isSuccess = false; - } - } - - @Override - protected void doStart() { - isReady = true; - LOGGER.info("Starting Dummy OK Process"); - while (isRunning) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - isRunning = false; - } - } - } - - @Override - protected void doTerminate() { - LOGGER.info("Terminating Dummy OK Process"); - this.isRunning = false; - } - - @Override - protected boolean doIsReady() { - return isReady; - } - - private boolean isSuccess() { - return isSuccess; - } - - public static int main(String[] args) { - Props props = new Props(new Properties()); - props.set(MonitoredProcess.NAME_PROPERTY, DummyOkProcess.class.getSimpleName()); - DummyOkProcess process = new DummyOkProcess(props); - process.start(); - return (process.isSuccess()) ? 1 : 0; - } -} diff --git a/server/process/sonar-dummy-app/src/main/resources/org/sonar/application/logback.xml b/server/process/sonar-dummy-app/src/main/resources/org/sonar/application/logback.xml deleted file mode 100644 index 933930557b9..00000000000 --- a/server/process/sonar-dummy-app/src/main/resources/org/sonar/application/logback.xml +++ /dev/null @@ -1,24 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" ?> - -<!-- - Configuration for default logger. Only used while embedded server is starting, - before proper logging configuration is loaded. - - See http://logback.qos.ch/manual/configuration.html ---> -<configuration debug="false"> - <contextListener class="ch.qos.logback.classic.jul.LevelChangePropagator"/> - - <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> - <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> - <pattern> - %d{yyyy.MM.dd HH:mm:ss} %-5level %msg%n - </pattern> - </encoder> - </appender> - - <root> - <level value="DEBUG"/> - <appender-ref ref="CONSOLE"/> - </root> -</configuration> diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Monitor.java b/server/process/sonar-process/src/main/java/org/sonar/process/Monitor.java deleted file mode 100644 index baa0f00791c..00000000000 --- a/server/process/sonar-process/src/main/java/org/sonar/process/Monitor.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.process; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.List; -import java.util.concurrent.CopyOnWriteArrayList; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -public class Monitor extends Thread implements Terminable { - - private static final Logger LOGGER = LoggerFactory.getLogger(Monitor.class); - - private static final long PING_DELAY_MS = 3000L; - - private long pingDelayMs = PING_DELAY_MS; - private final List<ProcessWrapper> processes = new CopyOnWriteArrayList<ProcessWrapper>(); - private final ScheduledFuture<?> watch; - private final ScheduledExecutorService monitorExecutionService; - - /** - * Starts another thread to send ping to all registered processes - */ - public Monitor() { - super("Process Monitor"); - monitorExecutionService = Executors.newScheduledThreadPool(1); - watch = monitorExecutionService.scheduleAtFixedRate(new ProcessWatch(), 0L, getPingDelayMs(), TimeUnit.MILLISECONDS); - } - - private long getPingDelayMs() { - return pingDelayMs; - } - - public Monitor setPingDelayMs(long pingDelayMs) { - this.pingDelayMs = pingDelayMs; - return this; - } - - private class ProcessWatch extends Thread { - private ProcessWatch() { - super("Process Ping"); - } - - @Override - public void run() { - for (ProcessWrapper process : processes) { - LOGGER.debug("Pinging process[{}]", process.getName()); - try { - ProcessMXBean mBean = process.getProcessMXBean(); - if (mBean != null) { - mBean.ping(); - } - } catch (Exception e) { - LOGGER.debug("Could not ping process[{}]", process.getName()); - LOGGER.trace("Ping failure", e); - } - } - } - } - - /** - * Registers and monitors process. Note that process is probably not ready yet. - */ - public void monitor(ProcessWrapper process) throws InterruptedException { - LOGGER.info("Monitoring process[{}]", process.getName()); - // starts a monitoring thread - process.start(); - processes.add(process); - } - - /** - * Check continuously that registered processes are still up. If any process is down or does not answer to pings - * during the max allowed period, then thread exits. - */ - @Override - public void run() { - try { - boolean ok = true; - while (isRunning && ok) { - LOGGER.debug("Monitoring {} processes.", processes.size()); - for (ProcessWrapper process : processes) { - if (!ProcessUtils.isAlive(process.process())) { - LOGGER.info("{} is down, stopping all other processes", process.getName()); - ok = false; - interrupt(); - } - } - if (ok) { - Thread.sleep(PING_DELAY_MS); - } - } - } catch (InterruptedException e) { - LOGGER.debug("Monitoring thread is interrupted"); - } finally { - terminate(); - } - } - - volatile Boolean isRunning = true; - - @Override - public synchronized void terminate() { - - LOGGER.debug("Monitoring thread is terminating"); - - if (!monitorExecutionService.isShutdown()) { - monitorExecutionService.shutdownNow(); - } - if (!watch.isCancelled()) { - watch.cancel(true); - } - - processes.clear(); - interruptAndWait(); - } - - private void interruptAndWait() { - this.interrupt(); - try { - if (this.isAlive()) { - this.join(); - } - } catch (InterruptedException e) { - // Expected to be interrupted :) - } - } -} diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java b/server/process/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java deleted file mode 100644 index 869867a6e34..00000000000 --- a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java +++ /dev/null @@ -1,375 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.process; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.annotation.CheckForNull; -import javax.annotation.Nullable; -import javax.management.JMX; -import javax.management.MBeanServerConnection; -import javax.management.Notification; -import javax.management.NotificationListener; -import javax.management.remote.JMXConnector; -import javax.management.remote.JMXConnectorFactory; -import javax.management.remote.JMXServiceURL; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -/** - * Fork and monitor a new process - */ -public class ProcessWrapper extends Thread implements Terminable { - - private static final Logger LOGGER = LoggerFactory.getLogger(ProcessWrapper.class); - - public static final long READY_TIMEOUT_MS = 300000L; - - private String processName, className; - private int jmxPort = -1; - private final List<String> javaOpts = new ArrayList<String>(); - private final List<String> classpath = new ArrayList<String>(); - private final Map<String, String> envProperties = new HashMap<String, String>(); - private final Properties properties = new Properties(); - private File workDir; - private Process process; - private StreamGobbler errorGobbler; - private StreamGobbler outputGobbler; - private ProcessMXBean processMXBean; - private final Object terminationLock = new Object(); - - public ProcessWrapper(String processName) { - super(processName); - this.processName = processName; - } - - public ProcessWrapper setClassName(String s) { - this.className = s; - return this; - } - - public ProcessWrapper setEnvProperty(String key, String value) { - envProperties.put(key, value); - return this; - } - - public ProcessWrapper addProperties(Properties p) { - properties.putAll(p); - return this; - } - - public ProcessWrapper addProperty(String key, String value) { - properties.setProperty(key, value); - return this; - } - - public ProcessWrapper addJavaOpts(String s) { - Collections.addAll(javaOpts, s.split(" ")); - return this; - } - - public ProcessWrapper addClasspath(String s) { - classpath.add(s); - return this; - } - - public ProcessWrapper setJmxPort(int i) { - this.jmxPort = i; - return this; - } - - public ProcessWrapper setWorkDir(File d) { - this.workDir = d; - return this; - } - - public ProcessWrapper setTempDirectory(File tempDirectory) { - this.setEnvProperty("java.io.tmpdir", tempDirectory.getAbsolutePath()); - return this; - } - - public ProcessWrapper setLogDir(File logDirectory) { - this.setEnvProperty("sonar.path.logs", logDirectory.getAbsolutePath()); - return this; - } - - @CheckForNull - public Process process() { - return process; - } - - /** - * Execute command-line and connects to JMX RMI. - * - * @return true on success, false if bad command-line or process failed to start JMX RMI - */ - public boolean execute() { - List<String> command = new ArrayList<String>(); - try { - command.add(buildJavaCommand()); - command.addAll(javaOpts); - command.addAll(buildJMXOptions()); - command.addAll(buildClasspath()); - command.add(className); - command.add(buildPropertiesFile().getAbsolutePath()); - - ProcessBuilder processBuilder = new ProcessBuilder(); - processBuilder.command(command); - processBuilder.directory(workDir); - processBuilder.environment().putAll(envProperties); - LOGGER.info("starting {}: {}", getName(), StringUtils.join(command, " ")); - process = processBuilder.start(); - errorGobbler = new StreamGobbler(process.getErrorStream(), this.getName() + "-ERROR"); - outputGobbler = new StreamGobbler(process.getInputStream(), this.getName()); - outputGobbler.start(); - errorGobbler.start(); - processMXBean = waitForJMX(); - if (processMXBean == null) { - terminate(); - return false; - } - return true; - } catch (Exception e) { - throw new IllegalStateException("Fail to start command: " + StringUtils.join(command, " "), e); - } - } - - @Override - public void run() { - try { - if (ProcessUtils.isAlive(process)) { - process.waitFor(); - } - } catch (Exception e) { - LOGGER.info("ProcessThread has been interrupted. Killing node."); - LOGGER.trace("Process exception", e); - } - } - - public boolean isReady() { - return processMXBean != null && processMXBean.isReady(); - } - - public ProcessMXBean getProcessMXBean() { - return processMXBean; - } - - private String buildJavaCommand() { - String separator = System.getProperty("file.separator"); - return new File(new File(System.getProperty("java.home")), - "bin" + separator + "java").getAbsolutePath(); - } - - private List<String> buildJMXOptions() { - if (jmxPort < 1) { - throw new IllegalStateException("JMX port is not set"); - } - return Arrays.asList( - "-Dcom.sun.management.jmxremote", - "-Dcom.sun.management.jmxremote.port=" + jmxPort, - "-Dcom.sun.management.jmxremote.authenticate=false", - "-Dcom.sun.management.jmxremote.ssl=false", - "-Djava.rmi.server.hostname=" + LoopbackAddress.get().getHostAddress()); - } - - private List<String> buildClasspath() { - return Arrays.asList("-cp", StringUtils.join(classpath, System.getProperty("path.separator"))); - } - - private File buildPropertiesFile() { - File propertiesFile = null; - try { - propertiesFile = File.createTempFile("sq-conf", "properties"); - Properties props = new Properties(); - props.putAll(properties); - props.put(MonitoredProcess.NAME_PROPERTY, processName); - OutputStream out = new FileOutputStream(propertiesFile); - props.store(out, "Temporary properties file for Process [" + getName() + "]"); - out.close(); - return propertiesFile; - } catch (IOException e) { - throw new IllegalStateException("Cannot write temporary settings to " + propertiesFile, e); - } - } - - /** - * Wait for JMX RMI to be ready. Return <code>null</code> - */ - @CheckForNull - private ProcessMXBean waitForJMX() { - JMXServiceURL jmxUrl = JmxUtils.serviceUrl(LoopbackAddress.get(), jmxPort); - for (int i = 0; i < 5; i++) { - try { - Thread.sleep(1000L); - JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, null); - jmxConnector.addConnectionNotificationListener(new NotificationListener() { - @Override - public void handleNotification(Notification notification, Object handback) { - LOGGER.debug("JMX Connection Notification:{}", notification.getMessage()); - } - }, null, null); - MBeanServerConnection mBeanServer = jmxConnector.getMBeanServerConnection(); - return JMX.newMBeanProxy(mBeanServer, JmxUtils.objectName(processName), ProcessMXBean.class); - } catch (Exception ignored) { - LOGGER.info(String.format("Could not connect to JMX (attempt %d). Trying again.", i), ignored); - } - } - // failed to connect - return null; - } - - @Override - public void terminate() { - synchronized (terminationLock) { - if (processMXBean != null && process != null) { - LOGGER.info("{} stopping", getName()); - // Send the terminate command to process in order to gracefully shutdown. - // Then hardly kill it if it didn't terminate in 30 seconds - ScheduledExecutorService killer = Executors.newScheduledThreadPool(1); - try { - Runnable killerTask = new Runnable() { - @Override - public void run() { - ProcessUtils.destroyQuietly(process); - } - }; - - ScheduledFuture killerFuture = killer.schedule(killerTask, 30, TimeUnit.SECONDS); - processMXBean.terminate(); - this.join(); - killerFuture.cancel(true); - LOGGER.info("{} stopped", getName()); - - } catch (Exception ignored) { - LOGGER.trace("Could not terminate process", ignored); - } finally { - killer.shutdownNow(); - interruptAndWait(); - } - } else { - // process is not monitored through JMX, but killing it though - ProcessUtils.destroyQuietly(process); - } - processMXBean = null; - } - } - - public boolean waitForReady() throws InterruptedException { - long now = 0; - long wait = 500L; - while (now < READY_TIMEOUT_MS) { - try { - if (processMXBean == null) { - return false; - } - if (processMXBean.isReady()) { - return true; - } - } catch (Exception e) { - LOGGER.trace("Process is not ready yet", e); - } - Thread.sleep(wait); - now += wait; - } - return false; - } - - private void interruptAndWait() { - try { - //after being interrupted, finalize the goblins - if (outputGobbler != null && outputGobbler.isAlive()) { - waitUntilFinish(outputGobbler); - } - - if (errorGobbler != null && errorGobbler.isAlive()) { - waitUntilFinish(errorGobbler); - } - if (process != null) { - ProcessUtils.closeStreams(process); - } - - //Join while the main thread terminates - if (this.isAlive()) { - this.join(); - } - } catch (InterruptedException e) { - // Expected to be interrupted :) - } - } - - private void waitUntilFinish(@Nullable Thread thread) { - if (thread != null && thread.isAlive()) { - try { - thread.join(); - } catch (InterruptedException e) { - LOGGER.error("InterruptedException while waiting finish of " + thread.getName() + " in process '" + getName() + "'", e); - } - } - } - - - private static class StreamGobbler extends Thread { - private final InputStream is; - private final Logger logger; - - StreamGobbler(InputStream is, String name) { - super(name + "_ProcessStreamGobbler"); - this.is = is; - this.logger = LoggerFactory.getLogger(name); - } - - @Override - public void run() { - InputStreamReader isr = new InputStreamReader(is); - BufferedReader br = new BufferedReader(isr); - try { - String line; - while ((line = br.readLine()) != null) { - logger.info(line); - } - } catch (Exception ignored) { - logger.trace("Error while Gobbling", ignored); - } finally { - IOUtils.closeQuietly(br); - IOUtils.closeQuietly(isr); - } - } - } -} diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessWrapperTest.java b/server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessWrapperTest.java deleted file mode 100644 index b217edb2584..00000000000 --- a/server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessWrapperTest.java +++ /dev/null @@ -1,118 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.process; - -import org.apache.commons.io.FileUtils; -import org.apache.commons.io.FilenameUtils; -import org.junit.Test; - -import java.io.File; -import java.util.Properties; - -import static org.fest.assertions.Assertions.assertThat; -import static org.fest.assertions.Fail.fail; - - -public class BaseProcessWrapperTest extends BaseProcessTest { - - @Test - public void has_dummy_app() { - assertThat(dummyAppJar).isFile(); - assertThat(dummyAppJar).exists(); - } - - private void assertCanStart(ProcessWrapper process) { - assertThat(process.execute()).isTrue(); - proc = process.process(); - } - - private void assertCanBeReady(ProcessWrapper process) throws InterruptedException { - int count = 0; - while (!process.isReady() && count < 5) { - Thread.sleep(500); - } - assertThat(process.getProcessMXBean().isReady()).isTrue(); - } - - private void assertPing(ProcessWrapper process) { - long now = System.currentTimeMillis(); - long ping = process.getProcessMXBean().ping(); - assertThat(ping - now).isLessThan(3000L); - } - - - @Test - public void execute_dummy_app() throws Exception { - - ProcessWrapper process = new ProcessWrapper("DummyOkProcess") - .addProperty(MonitoredProcess.NAME_PROPERTY, "DummyOkProcess") - .addClasspath(dummyAppJar.getAbsolutePath()) - .setWorkDir(temp.getRoot()) - .setTempDirectory(temp.getRoot()) - .setJmxPort(freePort) - .setClassName(DUMMY_OK_APP); - - assertThat(process.isAlive()).isFalse(); - assertCanStart(process); - process.start(); - assertCanBeReady(process); - assertThat(process.isAlive()).isTrue(); - assertPing(process); - process.terminate(); - try { - assertPing(process); - fail(); - } catch (Exception e) { - - } - } - - - @Test - public void execute_dummy_in_space_folder_app() throws Exception { - - // 0 create a home with space... - File home = temp.newFolder("t est"); - assertThat(home.canWrite()).isTrue(); - File lib = new File(home, "lib"); - File tempdir = new File(home, "temp"); - FileUtils.copyFileToDirectory(dummyAppJar, lib); - - // 1 Create Properties - Props props = new Props(new Properties()); - props.set("spaceHome", home.getAbsolutePath()); - - // 3 start dummy app - File effectiveHome = props.nonNullValueAsFile("spaceHome"); - - String cp = FilenameUtils.concat(new File(effectiveHome, "lib").getAbsolutePath(), "*"); - System.out.println("cp = " + cp); - ProcessWrapper process = new ProcessWrapper("DummyOkProcess") - .addProperty(MonitoredProcess.NAME_PROPERTY, "DummyOkProcess") - .setTempDirectory(tempdir) - .addClasspath(cp) - .setWorkDir(home) - .setJmxPort(freePort) - .setClassName(DUMMY_OK_APP); - - assertThat(process.isAlive()).isFalse(); - assertCanStart(process); - } -} diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/DummyProcess.java b/server/process/sonar-process/src/test/java/org/sonar/process/DummyProcess.java deleted file mode 100644 index f16c61fd93c..00000000000 --- a/server/process/sonar-process/src/test/java/org/sonar/process/DummyProcess.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.process; - -import org.apache.commons.io.FileUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.File; -import java.util.Properties; - -public class DummyProcess extends MonitoredProcess { - - public static final String NAME = "DummyName"; - public static final String CHECKFILE_NAME = "check.tmp"; - - private static final Logger LOGGER = LoggerFactory.getLogger(DummyProcess.class); - - private boolean isReady = false; - private boolean isRunning = true; - private File checkFile; - - - protected DummyProcess(Props props, boolean monitored) throws Exception { - super(props, monitored); - } - - protected DummyProcess(Props props) throws Exception { - super(props); - } - - @Override - protected void doStart() { - isReady = true; - checkFile = new File(FileUtils.getTempDirectory(), CHECKFILE_NAME); - LOGGER.info("Starting Dummy OK Process"); - while (isRunning) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - isRunning = false; - } - } - } - - @Override - protected void doTerminate() { - LOGGER.info("Terminating Dummy OK Process"); - this.isRunning = false; - } - - @Override - protected boolean doIsReady() { - return isReady; - } - - public static void main(String[] args) throws Exception { - Props props = new Props(new Properties()); - props.set(MonitoredProcess.NAME_PROPERTY, DummyProcess.class.getSimpleName()); - new DummyProcess(props).start(); - System.exit(1); - } - - public File getCheckFile() { - return checkFile; - } -} diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/MonitorTestBase.java b/server/process/sonar-process/src/test/java/org/sonar/process/MonitorTestBase.java deleted file mode 100644 index fb9c818d017..00000000000 --- a/server/process/sonar-process/src/test/java/org/sonar/process/MonitorTestBase.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.process; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; - -import static org.fest.assertions.Assertions.assertThat; - -public class MonitorTestBase extends BaseProcessTest { - - - Monitor monitor; - - @Before - public void setUpMonitor() throws Exception { - monitor = new Monitor(); - } - - @After - public void downMonitor() throws Exception { - if (monitor != null) { - monitor.interrupt(); - monitor = null; - } - } - - @Test - public void monitor_can_start_and_stop() { - assertThat(monitor.isAlive()).isFalse(); - monitor.start(); - assertThat(monitor.isAlive()).isTrue(); - monitor.terminate(); - assertThat(monitor.isAlive()).isFalse(); - } - - @Test(timeout = 2500L) - public void monitor_should_interrupt_process() throws Exception { - // 0 start the dummyProcess - ProcessWrapper process = new ProcessWrapper("DummyOkProcess") - .addProperty(MonitoredProcess.NAME_PROPERTY, "DummyOkProcess") - .addClasspath(dummyAppJar.getAbsolutePath()) - .setWorkDir(temp.getRoot()) - .setTempDirectory(temp.getRoot()) - .setJmxPort(freePort) - .setClassName(DUMMY_OK_APP); - - assertThat(process.execute()); - - - // 1 start my monitor & register process - monitor.start(); - monitor.monitor(process); - - // 2 terminate monitor, assert process is terminated - monitor.terminate(); - assertThat(monitor.isAlive()).isFalse(); - assertThat(process.isAlive()).isFalse(); - } -} diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/MonitoredProcessTest.java b/server/process/sonar-process/src/test/java/org/sonar/process/MonitoredProcessTest.java deleted file mode 100644 index c753bd00422..00000000000 --- a/server/process/sonar-process/src/test/java/org/sonar/process/MonitoredProcessTest.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * SonarQube, open source software quality management tool. - * Copyright (C) 2008-2014 SonarSource - * mailto:contact AT sonarsource DOT com - * - * SonarQube is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3 of the License, or (at your option) any later version. - * - * SonarQube is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program; if not, write to the Free Software Foundation, - * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - */ -package org.sonar.process; - -import org.junit.Test; - -import java.util.Properties; - -import static org.fest.assertions.Assertions.assertThat; -import static org.fest.assertions.Fail.fail; - -public class MonitoredProcessTest { - - @Test - public void fail_on_missing_name() throws Exception { - Properties properties = new Properties(); - - try { - new DummyProcess(new Props(properties), true); - fail(); - } catch (IllegalArgumentException e) { - assertThat(e.getMessage()).isEqualTo("Missing property: pName"); - } - - properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME); - DummyProcess dummyProcess = new DummyProcess(new Props(properties), true); - assertThat(dummyProcess).isNotNull(); - } - - @Test - public void should_not_monitor_debug() throws Exception { - Properties properties = new Properties(); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME); - DummyProcess dummyProcess = new DummyProcess(new Props(properties), false); - - assertThat(dummyProcess.isMonitored()).isFalse(); - } - - @Test - public void should_monitor_by_default() throws Exception { - Properties properties = new Properties(); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME); - properties.setProperty("sonar.search.javaOpts", "hello world"); - DummyProcess dummyProcess = new DummyProcess(new Props(properties)); - - assertThat(dummyProcess.isMonitored()).isTrue(); - } - - @Test(timeout = 3000L) - public void monitor_dies_when_no_pings() throws Exception { - Properties properties = new Properties(); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME); - final DummyProcess dummyProcess = new DummyProcess(new Props(properties), true); - dummyProcess.setTimeout(1000L) - .setCheckDelay(500L); - Thread process = new Thread(new Runnable() { - @Override - public void run() { - dummyProcess.start(); - } - }); - assertProcessNotYetRunning(dummyProcess); - process.start(); - Thread.sleep(100); - - assertProcessRunning(dummyProcess); - assertJoinAndTerminate(dummyProcess, process); - } - - @Test(timeout = 3000L) - public void monitor_dies_after_stopping_to_ping() throws Exception { - Properties properties = new Properties(); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME); - final DummyProcess dummyProcess = new DummyProcess(new Props(properties), true); - dummyProcess.setTimeout(1000L) - .setCheckDelay(500L); - Thread process = new Thread(new Runnable() { - @Override - public void run() { - dummyProcess.start(); - } - }); - assertProcessNotYetRunning(dummyProcess); - process.start(); - Thread.sleep(100); - - int count = 0; - for (int i = 0; i < 3; i++) { - dummyProcess.ping(); - assertProcessRunning(dummyProcess); - Thread.sleep(300); - count++; - } - assertThat(count).isEqualTo(3); - assertJoinAndTerminate(dummyProcess, process); - } - - @Test(timeout = 3000L) - public void monitor_explicitly_shutdown() throws Exception { - Properties properties = new Properties(); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME); - final DummyProcess dummyProcess = new DummyProcess(new Props(properties), true); - dummyProcess.setTimeout(Long.MAX_VALUE).setCheckDelay(500L); - Thread process = new Thread(new Runnable() { - @Override - public void run() { - dummyProcess.start(); - } - }); - assertProcessNotYetRunning(dummyProcess); - process.start(); - Thread.sleep(100); - assertProcessRunning(dummyProcess); - dummyProcess.terminate(); - Thread.sleep(100); - assertProcessTerminated(dummyProcess); - } - - @Test(timeout = 1000L) - public void process_does_not_die_when_debugged() throws Exception { - Properties properties = new Properties(); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, DummyProcess.NAME); - - final DummyProcess dummyProcess = new DummyProcess(new Props(properties), false); - assertThat(dummyProcess.isMonitored()).isFalse(); - - dummyProcess.setTimeout(100L).setCheckDelay(100L); - Thread process = new Thread(new Runnable() { - @Override - public void run() { - dummyProcess.start(); - } - }); - process.start(); - Thread.sleep(600); - - assertProcessRunning(dummyProcess); - dummyProcess.terminate(); - assertProcessTerminated(dummyProcess); - } - - private void assertJoinAndTerminate(DummyProcess dummyProcess, Thread process) throws InterruptedException { - process.join(); - assertProcessTerminated(dummyProcess); - } - - private void assertProcessTerminated(DummyProcess dummyProcess) { - assertThat(dummyProcess.isReady()).isTrue(); - assertThat(dummyProcess.isTerminated()).isTrue(); - assertProcessCreatedFile(dummyProcess); - } - - private void assertProcessNotYetRunning(DummyProcess dummyProcess) { - assertThat(dummyProcess.isReady()).isFalse(); - assertThat(dummyProcess.isTerminated()).isFalse(); - } - - private void assertProcessRunning(DummyProcess dummyProcess) throws InterruptedException { - assertThat(dummyProcess.isReady()).isTrue(); - assertThat(dummyProcess.isTerminated()).isFalse(); - } - - private void assertProcessCreatedFile(DummyProcess dummyProcess) { - assertThat(dummyProcess.getCheckFile()).isNotNull(); - assertThat(dummyProcess.getCheckFile().getName()).isEqualTo(DummyProcess.CHECKFILE_NAME); - } - -} diff --git a/server/process/sonar-dummy-app/pom.xml b/server/sonar-process-monitor/pom.xml index 9c0f067ebbb..fa0869a52b8 100644 --- a/server/process/sonar-dummy-app/pom.xml +++ b/server/sonar-process-monitor/pom.xml @@ -4,16 +4,14 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.codehaus.sonar</groupId> - <artifactId>process</artifactId> - <version>5.0-SNAPSHOT</version> + <artifactId>server</artifactId> + <version>4.5-SNAPSHOT</version> <relativePath>../</relativePath> </parent> <modelVersion>4.0.0</modelVersion> - <artifactId>sonar-dummy-app</artifactId> - <packaging>jar</packaging> - <name>SonarQube :: Process :: DummyApp</name> - <description>Dummy Application to test sonar-process</description> + <artifactId>sonar-process-monitor</artifactId> + <name>SonarQube :: Process Monitor</name> <dependencies> <dependency> @@ -22,23 +20,32 @@ <version>${project.version}</version> </dependency> <dependency> - <groupId>com.google.code.findbugs</groupId> - <artifactId>jsr305</artifactId> - <scope>provided</scope> - </dependency> - - - <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> - <scope>runtime</scope> </dependency> - <!-- testing --> + <dependency> + <groupId>commons-codec</groupId> + <artifactId>commons-codec</artifactId> + </dependency> + <dependency> + <groupId>commons-io</groupId> + <artifactId>commons-io</artifactId> + </dependency> + <dependency> + <groupId>commons-lang</groupId> + <artifactId>commons-lang</artifactId> + </dependency> + <dependency> + <groupId>com.google.code.findbugs</groupId> + <artifactId>jsr305</artifactId> + <scope>provided</scope> + </dependency> + <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> @@ -59,42 +66,22 @@ <artifactId>hamcrest-all</artifactId> <scope>test</scope> </dependency> - </dependencies> - <build> - <plugins> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-jar-plugin</artifactId> - <configuration> - <archive> - <manifest> - <addClasspath>false</addClasspath> - <mainClass>org.sonar.application.DummyOkProcess</mainClass> - </manifest> - </archive> - </configuration> - </plugin> - <plugin> - <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-shade-plugin</artifactId> - <version>2.3</version> - <executions> - <execution> - <phase>package</phase> - <goals> - <goal>shade</goal> - </goals> - <configuration> - <keepDependenciesWithProvidedScope>false</keepDependenciesWithProvidedScope> - <minimizeJar>true</minimizeJar> - </configuration> - </execution> - </executions> - </plugin> - </plugins> - </build> + <dependency> + <groupId>com.google.guava</groupId> + <artifactId>guava</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.codehaus.sonar</groupId> + <artifactId>sonar-process</artifactId> + <type>test-jar</type> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>com.github.kevinsawicki</groupId> + <artifactId>http-request</artifactId> + <scope>test</scope> + </dependency> - <properties> - <sonar.exclusions>**/*.java</sonar.exclusions> - </properties> + </dependencies> </project> diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java new file mode 100644 index 00000000000..e950c92c58c --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaCommand.java @@ -0,0 +1,181 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import javax.annotation.Nullable; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +public class JavaCommand { + + // unique key among the group of commands to launch + private final String key; + + private File workDir; + + // any available port by default + private int jmxPort = -1; + + // for example -Xmx1G + private final List<String> javaOptions = new ArrayList<String>(); + + // entry point + private String className; + + // relative path to JAR files + private final List<String> classpath = new ArrayList<String>(); + + // program arguments (parameters of main(String[]) + private final Map<String, String> arguments = new LinkedHashMap<String, String>(); + + private final Map<String, String> envVariables = new HashMap<String, String>(System.getenv()); + + public JavaCommand(String key) { + this.key = key; + } + + public String getKey() { + return key; + } + + public File getWorkDir() { + return workDir; + } + + public JavaCommand setWorkDir(File workDir) { + this.workDir = workDir; + return this; + } + + /** + * Shortcut to set the java option -Djava.io.tmpdir + */ + public JavaCommand setTempDir(File tempDir) { + this.javaOptions.add("-Djava.io.tmpdir=" + tempDir.getAbsolutePath()); + return this; + } + + public int getJmxPort() { + return jmxPort; + } + + /** + * Current mandatory to be set + */ + public JavaCommand setJmxPort(int jmxPort) { + this.jmxPort = jmxPort; + return this; + } + + public List<String> getJavaOptions() { + return javaOptions; + } + + public JavaCommand addJavaOption(String s) { + javaOptions.add(s); + return this; + } + + public JavaCommand addJavaOptions(String s) { + Collections.addAll(javaOptions, s.split(" ")); + return this; + } + + public String getClassName() { + return className; + } + + public JavaCommand setClassName(String className) { + this.className = className; + return this; + } + + public List<String> getClasspath() { + return classpath; + } + + public JavaCommand addClasspath(String s) { + classpath.add(s); + return this; + } + + public Map<String, String> getArguments() { + return arguments; + } + + public JavaCommand setArgument(String key, @Nullable String value) { + if (value == null) { + arguments.remove(key); + } else { + arguments.put(key, value); + } + return this; + } + + public JavaCommand setArguments(Properties args) { + for (Map.Entry<Object, Object> entry : args.entrySet()) { + setArgument(entry.getKey().toString(), entry.getValue() != null ? entry.getValue().toString() : null); + } + return this; + } + + public Map<String, String> getEnvVariables() { + return envVariables; + } + + public JavaCommand setEnvVariable(String key, @Nullable String value) { + if (value == null) { + envVariables.remove(key); + } else { + envVariables.put(key, value); + } + return this; + } + + public boolean isDebugMode() { + for (String javaOption : javaOptions) { + if (javaOption.contains("-agentlib:jdwp")) { + return true; + } + } + return false; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("JavaCommand{"); + sb.append("workDir=").append(workDir); + sb.append(", jmxPort=").append(jmxPort); + sb.append(", javaOptions=").append(javaOptions); + sb.append(", className='").append(className).append('\''); + sb.append(", classpath=").append(classpath); + sb.append(", arguments=").append(arguments); + sb.append(", envVariables=").append(envVariables); + sb.append('}'); + return sb.toString(); + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java new file mode 100644 index 00000000000..ff8ef88e8f7 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JavaProcessLauncher.java @@ -0,0 +1,124 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.LoggerFactory; +import org.sonar.process.LoopbackAddress; +import org.sonar.process.ProcessEntryPoint; +import org.sonar.process.ProcessUtils; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +public class JavaProcessLauncher { + + private final Timeouts timeouts; + + public JavaProcessLauncher(Timeouts timeouts) { + this.timeouts = timeouts; + } + + ProcessRef launch(JavaCommand command) { + Process process = null; + try { + ProcessBuilder processBuilder = create(command); + LoggerFactory.getLogger(getClass()).info("Launch {}: {}", + command.getKey(), StringUtils.join(processBuilder.command(), " ")); + process = processBuilder.start(); + StreamGobbler errorGobbler = new StreamGobbler(process.getErrorStream(), command.getKey()); + StreamGobbler inputGobbler = new StreamGobbler(process.getInputStream(), command.getKey()); + inputGobbler.start(); + errorGobbler.start(); + + return new ProcessRef(command.getKey(), process, errorGobbler, inputGobbler); + + } catch (Exception e) { + // just in case + ProcessUtils.destroyQuietly(process); + throw new IllegalStateException("Fail to launch " + command.getKey(), e); + } + } + + private ProcessBuilder create(JavaCommand javaCommand) { + List<String> commands = new ArrayList<String>(); + commands.add(buildJavaPath()); + commands.addAll(javaCommand.getJavaOptions()); + commands.addAll(buildJmxOptions(javaCommand)); + commands.addAll(buildClasspath(javaCommand)); + commands.add(javaCommand.getClassName()); + + // TODO warning - does it work if temp dir contains a whitespace ? + commands.add(buildPropertiesFile(javaCommand).getAbsolutePath()); + + ProcessBuilder processBuilder = new ProcessBuilder(); + processBuilder.command(commands); + processBuilder.directory(javaCommand.getWorkDir()); + processBuilder.environment().putAll(javaCommand.getEnvVariables()); + return processBuilder; + } + + private String buildJavaPath() { + String separator = System.getProperty("file.separator"); + return new File(new File(System.getProperty("java.home")), + "bin" + separator + "java").getAbsolutePath(); + } + + private List<String> buildJmxOptions(JavaCommand javaCommand) { + if (javaCommand.getJmxPort() < 1) { + throw new IllegalStateException("JMX port is not set"); + } + return Arrays.asList( + "-Dcom.sun.management.jmxremote", + "-Dcom.sun.management.jmxremote.port=" + javaCommand.getJmxPort(), + "-Dcom.sun.management.jmxremote.authenticate=false", + "-Dcom.sun.management.jmxremote.ssl=false", + "-Djava.rmi.server.hostname=" + LoopbackAddress.get().getHostAddress()); + } + + private List<String> buildClasspath(JavaCommand javaCommand) { + return Arrays.asList("-cp", StringUtils.join(javaCommand.getClasspath(), System.getProperty("path.separator"))); + } + + private File buildPropertiesFile(JavaCommand javaCommand) { + File propertiesFile = null; + try { + propertiesFile = File.createTempFile("sq-conf", "properties"); + Properties props = new Properties(); + props.putAll(javaCommand.getArguments()); + props.setProperty(ProcessEntryPoint.PROPERTY_PROCESS_KEY, javaCommand.getKey()); + props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, String.valueOf(javaCommand.isDebugMode())); + props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_TIMEOUT, String.valueOf(timeouts.getAutokillPingTimeout())); + props.setProperty(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_INTERVAL, String.valueOf(timeouts.getAutokillPingInterval())); + props.setProperty(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, String.valueOf(timeouts.getTerminationTimeout())); + OutputStream out = new FileOutputStream(propertiesFile); + props.store(out, String.format("Temporary properties file for command [%s]", javaCommand.getKey())); + out.close(); + return propertiesFile; + } catch (Exception e) { + throw new IllegalStateException("Cannot write temporary settings to " + propertiesFile, e); + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java new file mode 100644 index 00000000000..c996595ba73 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/JmxConnector.java @@ -0,0 +1,44 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +/** + * Interactions with monitored process + */ +public interface JmxConnector { + + /** + * Throws an exception if timeout reached + */ + void connect(JavaCommand command, ProcessRef processRef, long timeoutMs); + + void ping(ProcessRef process); + + /** + * Throws an exception if timeout reached + */ + boolean isReady(ProcessRef process, long timeoutMs); + + /** + * Throws an exception if timeout reached + */ + void terminate(ProcessRef process, long timeoutMs); + +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java new file mode 100644 index 00000000000..518d2c354cd --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Monitor.java @@ -0,0 +1,211 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.slf4j.LoggerFactory; +import org.sonar.process.Lifecycle; +import org.sonar.process.MessageException; +import org.sonar.process.State; +import org.sonar.process.SystemExit; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class Monitor { + + private final List<ProcessRef> processes = new CopyOnWriteArrayList<ProcessRef>(); + private final TerminatorThread terminator; + private final JavaProcessLauncher launcher; + private final JmxConnector jmxConnector; + private final Lifecycle lifecycle = new Lifecycle(); + private final Timeouts timeouts; + + private final SystemExit systemExit; + private Thread shutdownHook = new Thread(new MonitorShutdownHook(), "Monitor Shutdown Hook"); + + // used by awaitTermination() to block until all processes are shutdown + private final List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<WatcherThread>(); + + Monitor(JavaProcessLauncher launcher, JmxConnector jmxConnector, Timeouts timeouts, SystemExit exit) { + this.launcher = launcher; + this.jmxConnector = jmxConnector; + this.timeouts = timeouts; + this.terminator = new TerminatorThread(processes, jmxConnector, timeouts); + this.systemExit = exit; + } + + public static Monitor create() { + Timeouts timeouts = new Timeouts(); + return new Monitor(new JavaProcessLauncher(timeouts), new RmiJmxConnector(), + timeouts, new SystemExit()); + } + + /** + * Starts commands and blocks current thread until all processes are in state {@link State#STARTED}. + * @throws java.lang.IllegalArgumentException if commands list is empty + * @throws java.lang.IllegalStateException if already started or if at least one process failed to start. In this case + * all processes are terminated. No need to execute {@link #stop()} + */ + public void start(List<JavaCommand> commands) { + if (commands.isEmpty()) { + throw new IllegalArgumentException("At least one command is required"); + } + + if (!lifecycle.tryToMoveTo(State.STARTING)) { + throw new IllegalStateException("Can not start multiple times"); + } + + // intercepts CTRL-C + Runtime.getRuntime().addShutdownHook(shutdownHook); + + for (JavaCommand command : commands) { + try { + ProcessRef processRef = launcher.launch(command); + monitor(command, processRef); + } catch (RuntimeException e) { + // fail to start or to monitor + stop(); + throw e; + } + } + + if (!lifecycle.tryToMoveTo(State.STARTED)) { + // stopping or stopped during startup, for instance : + // 1. A is started + // 2. B starts + // 3. A crashes while B is starting + // 4. if B was not monitored during Terminator execution, then it's an alive orphan + stop(); + throw new IllegalStateException("Stopped during startup"); + } + } + + private void monitor(JavaCommand command, ProcessRef processRef) { + // physically watch if process is alive + WatcherThread watcherThread = new WatcherThread(processRef, this); + watcherThread.start(); + watcherThreads.add(watcherThread); + + // add to list of monitored processes only when successfully connected to it + jmxConnector.connect(command, processRef, timeouts.getJmxConnectionTimeout()); + processes.add(processRef); + + // ping process on a regular basis + processRef.setPingEnabled(!command.isDebugMode()); + if (processRef.isPingEnabled()) { + PingerThread.startPinging(processRef, jmxConnector, timeouts); + } + + // wait for process to be ready (accept requests or so on) + waitForReady(processRef); + + LoggerFactory.getLogger(getClass()).info(String.format("%s is up", processRef)); + } + + private void waitForReady(ProcessRef processRef) { + boolean ready = false; + while (!ready) { + if (processRef.isTerminated()) { + throw new MessageException(String.format("%s failed to start", processRef)); + } + try { + ready = jmxConnector.isReady(processRef, timeouts.getMonitorIsReadyTimeout()); + } catch (Exception ignored) { + // failed to send request, probably because RMI server is still not alive + // trying again, as long as process is alive + // TODO could be improved to have a STARTING timeout (to be implemented in monitor or + // in child process ?) + } + try { + Thread.sleep(300L); + } catch (InterruptedException e) { + throw new IllegalStateException("Interrupted while waiting for " + processRef + " to be ready", e); + } + } + } + + /** + * Blocks until all processes are terminated + */ + public void awaitTermination() { + for (WatcherThread watcherThread : watcherThreads) { + while (watcherThread.isAlive()) { + try { + watcherThread.join(); + } catch (InterruptedException ignored) { + // ignore, stop blocking + } + } + } + } + + /** + * Blocks until all processes are terminated. + */ + public void stop() { + terminateAsync(); + try { + terminator.join(); + } catch (InterruptedException ignored) { + // ignore, stop blocking + } + // safeguard if TerminatorThread is buggy + hardKillAll(); + lifecycle.tryToMoveTo(State.STOPPED); + systemExit.exit(0); + } + + /** + * Asks for processes termination and returns without blocking until termination. + * @return true if termination was requested, false if it was already being terminated + */ + boolean terminateAsync() { + boolean requested = false; + if (lifecycle.tryToMoveTo(State.STOPPING)) { + requested = true; + terminator.start(); + } + return requested; + } + + private void hardKillAll() { + // no specific order, kill'em all!!! + for (ProcessRef process : processes) { + process.hardKill(); + } + } + + public State getState() { + return lifecycle.getState(); + } + + Thread getShutdownHook() { + return shutdownHook; + } + + private class MonitorShutdownHook implements Runnable { + @Override + public void run() { + systemExit.setInShutdownHook(); + // blocks until everything is corrected terminated + stop(); + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java new file mode 100644 index 00000000000..65df8546e33 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/PingerThread.java @@ -0,0 +1,60 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +/** + * This thread pings a process - through RMI - at fixed delay + */ +class PingerThread extends Thread { + + private final ProcessRef processRef; + private final JmxConnector jmxConnector; + + private PingerThread(ProcessRef process, JmxConnector jmxConnector) { + // it's important to give a name for traceability in profiling tools like visualVM + super(String.format("Ping[%s]", process.getKey())); + setDaemon(true); + this.processRef = process; + this.jmxConnector = jmxConnector; + } + + @Override + public void run() { + if (!processRef.isTerminated() && processRef.isPingEnabled()) { + try { + jmxConnector.ping(processRef); + } catch (Exception ignored) { + // failed to ping + } + } else { + interrupt(); + } + } + + static void startPinging(ProcessRef processRef, JmxConnector jmxConnector, Timeouts timeouts) { + ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1); + PingerThread pinger = new PingerThread(processRef, jmxConnector); + scheduler.scheduleAtFixedRate(pinger, 0L, timeouts.getMonitorPingInterval(), TimeUnit.MILLISECONDS); + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java new file mode 100644 index 00000000000..8d926c39d72 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/ProcessRef.java @@ -0,0 +1,98 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.sonar.process.ProcessUtils; + +class ProcessRef { + + private final String key; + private final Process process; + private final StreamGobbler[] gobblers; + private volatile boolean terminated = false; + private volatile boolean pingEnabled = true; + + ProcessRef(String key, Process process, StreamGobbler... gobblers) { + this.key = key; + this.process = process; + this.terminated = !ProcessUtils.isAlive(process); + this.gobblers = gobblers; + } + + /** + * Unique logical key (not the pid), for instance "ES" + */ + String getKey() { + return key; + } + + /** + * The {@link java.lang.Process} + */ + Process getProcess() { + return process; + } + + /** + * Almost real-time status + */ + boolean isTerminated() { + return terminated; + } + + /** + * Sending pings can be disabled when requesting for termination or when process is on debug mode (JDWP) + */ + void setPingEnabled(boolean b) { + this.pingEnabled = b; + } + + boolean isPingEnabled() { + return pingEnabled; + } + + /** + * Destroy the process without gracefully asking it to terminate (kill -9). + * @return true if the process was killed, false if process is already terminated + */ + boolean hardKill() { + boolean killed = false; + terminated = true; + pingEnabled = false; + if (ProcessUtils.isAlive(process)) { + ProcessUtils.destroyQuietly(process); + killed = true; + } + for (StreamGobbler gobbler : gobblers) { + StreamGobbler.waitUntilFinish(gobbler); + } + ProcessUtils.closeStreams(process); + return killed; + } + + void setTerminated(boolean b) { + this.terminated = b; + } + + @Override + public String toString() { + return String.format("Process[%s]", key); + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java new file mode 100644 index 00000000000..876ccc2b22f --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/RmiJmxConnector.java @@ -0,0 +1,147 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.slf4j.LoggerFactory; +import org.sonar.process.JmxUtils; +import org.sonar.process.LoopbackAddress; +import org.sonar.process.ProcessMXBean; +import org.sonar.process.ProcessUtils; + +import javax.annotation.CheckForNull; +import javax.management.JMX; +import javax.management.MBeanServerConnection; +import javax.management.remote.JMXConnector; +import javax.management.remote.JMXConnectorFactory; +import javax.management.remote.JMXServiceURL; + +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +class RmiJmxConnector implements JmxConnector { + + static { + /* + * Prevents such warnings : + * + * WARNING: Failed to restart: java.io.IOException: Failed to get a RMI stub: javax.naming.ServiceUnavailableException [Root exception + * is java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is: + * java.net.ConnectException: Connection refused] + * Sep 11, 2014 7:32:32 PM RMIConnector RMIClientCommunicatorAdmin-doStop + * WARNING: Failed to call the method close():java.rmi.ConnectException: Connection refused to host: 127.0.0.1; nested exception is: + * java.net.ConnectException: Connection refused + * Sep 11, 2014 7:32:32 PM ClientCommunicatorAdmin Checker-run + * WARNING: Failed to check connection: java.net.ConnectException: Connection refused + * Sep 11, 2014 7:32:32 PM ClientCommunicatorAdmin Checker-run + * WARNING: stopping + */ + System.setProperty("sun.rmi.transport.tcp.logLevel", "SEVERE"); + } + + private final Map<ProcessRef, ProcessMXBean> mbeans = new IdentityHashMap<ProcessRef, ProcessMXBean>(); + + @Override + public synchronized void connect(final JavaCommand command, ProcessRef processRef, long timeoutMs) { + ConnectorCallable callable = new ConnectorCallable(command, processRef.getProcess()); + ProcessMXBean mxBean = execute(callable, timeoutMs); + if (mxBean != null) { + register(processRef, mxBean); + } else if (!processRef.isTerminated()) { + throw new IllegalStateException("Fail to connect to JMX RMI server of " + processRef); + } + } + + @Override + public void ping(ProcessRef processRef) { + mbeans.get(processRef).ping(); + } + + @Override + public boolean isReady(final ProcessRef processRef, long timeoutMs) { + return execute(new Callable<Boolean>() { + @Override + public Boolean call() throws Exception { + return mbeans.get(processRef).isReady(); + } + }, timeoutMs); + } + + @Override + public void terminate(final ProcessRef processRef, long timeoutMs) { + execute(new Callable() { + @Override + public Void call() throws Exception { + LoggerFactory.getLogger(getClass()).info("Request termination of " + processRef); + mbeans.get(processRef).terminate(); + return null; + } + }, timeoutMs); + } + + void register(ProcessRef processRef, ProcessMXBean mxBean) { + mbeans.put(processRef, mxBean); + } + + private <T> T execute(Callable<T> callable, long timeoutMs) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + try { + Future<T> future = executor.submit(callable); + return future.get(timeoutMs, TimeUnit.MILLISECONDS); + } catch (Exception e) { + throw new IllegalStateException("Fail send JMX request", e); + } finally { + executor.shutdownNow(); + } + } + + private static class ConnectorCallable implements Callable<ProcessMXBean> { + private final JavaCommand command; + private final Process process; + + private ConnectorCallable(JavaCommand command, Process process) { + this.command = command; + this.process = process; + } + + @Override + @CheckForNull + public ProcessMXBean call() throws Exception { + JMXServiceURL jmxUrl = JmxUtils.serviceUrl(LoopbackAddress.get(), command.getJmxPort()); + while (ProcessUtils.isAlive(process)) { + try { + JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, null); + MBeanServerConnection mBeanServer = jmxConnector.getMBeanServerConnection(); + return JMX.newMBeanProxy(mBeanServer, JmxUtils.objectName(command.getKey()), ProcessMXBean.class); + } catch (Exception ignored) { + // ignored, RMI server is probably not started yet + } + Thread.sleep(300L); + } + + // process went down, no need to connect + return null; + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java new file mode 100644 index 00000000000..55d95c8a467 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/StreamGobbler.java @@ -0,0 +1,69 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.annotation.Nullable; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; + +/** + * Reads process output and writes to logs + */ +class StreamGobbler extends Thread { + + private final InputStream is; + private final Logger logger; + + StreamGobbler(InputStream is, String processKey) { + super(String.format("Gobbler[%s]", processKey)); + this.is = is; + this.logger = LoggerFactory.getLogger(processKey); + } + + @Override + public void run() { + BufferedReader br = new BufferedReader(new InputStreamReader(is)); + try { + String line; + while ((line = br.readLine()) != null) { + logger.info(line); + } + } catch (Exception ignored) { + + } finally { + IOUtils.closeQuietly(br); + } + } + + static void waitUntilFinish(@Nullable StreamGobbler gobbler) { + if (gobbler != null) { + try { + gobbler.join(); + } catch (InterruptedException ignored) { + } + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java new file mode 100644 index 00000000000..f5b11b3148f --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/TerminatorThread.java @@ -0,0 +1,63 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.slf4j.LoggerFactory; + +import java.util.List; + +/** + * Terminates all monitored processes. Tries to gracefully terminate each process, + * then kill if timeout expires. Ping monitoring is disabled so process auto kills (self graceful termination, else self kill) + * if it does not receive the termination request. + */ +class TerminatorThread extends Thread { + + private final List<ProcessRef> processes; + private final JmxConnector jmxConnector; + private final Timeouts timeouts; + + TerminatorThread(List<ProcessRef> processes, JmxConnector jmxConnector, Timeouts timeouts) { + super("Terminator"); + this.processes = processes; + this.jmxConnector = jmxConnector; + this.timeouts = timeouts; + } + + @Override + public void run() { + // terminate in reverse order of startup (dependency order) + for (int index = processes.size() - 1; index >= 0; index--) { + final ProcessRef processRef = processes.get(index); + if (!processRef.isTerminated()) { + processRef.setPingEnabled(false); + try { + jmxConnector.terminate(processRef, timeouts.getTerminationTimeout()); + } catch (Exception ignored) { + // failed to gracefully stop in a timely fashion + LoggerFactory.getLogger(getClass()).info(String.format("Kill %s", processRef)); + } finally { + // kill even if graceful termination was done, just to be sure that physical process is really down + processRef.hardKill(); + } + } + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java new file mode 100644 index 00000000000..68934f21066 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/Timeouts.java @@ -0,0 +1,118 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +/** + * Most of the timeouts involved in process monitoring, in milliseconds + */ +class Timeouts { + + private long terminationTimeout = 120000L; + private long jmxConnectionTimeout = 30000L; + private long monitorPingInterval = 3000L; + private long monitorIsReadyTimeout = 10000L; + private long autokillPingTimeout = 60000L; + private long autokillPingInterval = 3000L; + + /** + * [monitor] Timeout to get connected to RMI MXBean while process is alive + */ + long getJmxConnectionTimeout() { + return jmxConnectionTimeout; + } + + /** + * @see #getJmxConnectionTimeout() + */ + void setJmxConnectionTimeout(long l) { + this.jmxConnectionTimeout = l; + } + + /** + * [monitor] Delay between each ping request + */ + long getMonitorPingInterval() { + return monitorPingInterval; + } + + /** + * @see #getMonitorPingInterval() + */ + void setMonitorPingInterval(long l) { + this.monitorPingInterval = l; + } + + /** + * [monitor] Timeout of isReady request + */ + long getMonitorIsReadyTimeout() { + return monitorIsReadyTimeout; + } + + /** + * @see #getMonitorIsReadyTimeout() + */ + void setMonitorIsReadyTimeout(long l) { + this.monitorIsReadyTimeout = l; + } + + /** + * [monitored process] maximum age of last received ping before process autokills + */ + long getAutokillPingTimeout() { + return autokillPingTimeout; + } + + /** + * @see #getAutokillPingTimeout() + */ + void setAutokillPingTimeout(long l) { + this.autokillPingTimeout = l; + } + + /** + * [monitored process] delay between checks of freshness of received pings + */ + long getAutokillPingInterval() { + return autokillPingInterval; + } + + /** + * @see #getAutokillPingInterval() + */ + void setAutokillPingInterval(long l) { + this.autokillPingInterval = l; + } + + /** + * [both monitor and monitored process] timeout of graceful termination before hard killing + */ + long getTerminationTimeout() { + return terminationTimeout; + } + + /** + * @see #getTerminationTimeout() + */ + void setTerminationTimeout(long l) { + this.terminationTimeout = l; + } + +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java new file mode 100644 index 00000000000..a0b92f5ddc8 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/WatcherThread.java @@ -0,0 +1,70 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.slf4j.LoggerFactory; +import org.sonar.process.ProcessUtils; + +/** + * This thread blocks as long as the monitored process is physically alive. + * It avoids from executing {@link Process#exitValue()} at a fixed rate : + * <ul> + * <li>no usage of exception for flow control. Indeed {@link Process#exitValue()} throws an exception + * if process is alive. There's no method <code>Process#isAlive()</code></li> + * <li>no delay, instantaneous notification that process is down</li> + * </ul> + */ +class WatcherThread extends Thread { + + private final ProcessRef process; + private final Monitor monitor; + + WatcherThread(ProcessRef processRef, Monitor monitor) { + // this name is different than Thread#toString(), which includes name, priority + // and thread group + // -> do not override toString() + super(String.format("Watch[%s]", processRef.getKey())); + this.process = processRef; + this.monitor = monitor; + } + + @Override + public void run() { + boolean alive = true; + while (alive) { + try { + process.getProcess().waitFor(); + process.setTerminated(true); + LoggerFactory.getLogger(getClass()).info(process + " is down"); + // terminate all other processes, but in another thread + monitor.stop(); + alive = false; + } catch (InterruptedException ignored) { + if (ProcessUtils.isAlive(process.getProcess())) { + LoggerFactory.getLogger(getClass()).error(String.format( + "Watcher of [%s] was interrupted but process is still alive. Killing it.", process.getKey())); + } + alive = false; + } finally { + process.hardKill(); + } + } + } +} diff --git a/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java new file mode 100644 index 00000000000..93fcb831c83 --- /dev/null +++ b/server/sonar-process-monitor/src/main/java/org/sonar/process/monitor/package-info.java @@ -0,0 +1,23 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +@ParametersAreNonnullByDefault +package org.sonar.process.monitor; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java new file mode 100644 index 00000000000..2010e19aa87 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/CallVerifierJmxConnector.java @@ -0,0 +1,34 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +/** + * Used to verify that pings were sent or not. + */ +public class CallVerifierJmxConnector extends RmiJmxConnector { + + boolean askedPing = false; + + @Override + public void ping(ProcessRef process) { + askedPing = true; + super.ping(process); + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java new file mode 100644 index 00000000000..055e3c8e50b --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/ImpossibleToConnectJmxConnector.java @@ -0,0 +1,42 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +public class ImpossibleToConnectJmxConnector implements JmxConnector { + @Override + public void connect(JavaCommand command, ProcessRef processRef, long timeoutMs) { + throw new IllegalStateException("Test - Impossible to connect to JMX"); + } + + @Override + public void ping(ProcessRef process) { + + } + + @Override + public boolean isReady(ProcessRef process, long timeoutMs) { + return false; + } + + @Override + public void terminate(ProcessRef process, long timeoutMs) { + + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java new file mode 100644 index 00000000000..766f24c86fc --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaCommandTest.java @@ -0,0 +1,79 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +import java.io.File; +import java.util.Properties; + +import static org.fest.assertions.Assertions.assertThat; + +public class JavaCommandTest { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void test_parameters() throws Exception { + JavaCommand command = new JavaCommand("es"); + + command.setArgument("first_arg", "val1"); + Properties args = new Properties(); + args.setProperty("second_arg", "val2"); + command.setArguments(args); + + command.setJmxPort(1234); + command.setClassName("org.sonar.ElasticSearch"); + command.setEnvVariable("JAVA_COMMAND_TEST", "1000"); + File tempDir = temp.newFolder(); + command.setTempDir(tempDir); + File workDir = temp.newFolder(); + command.setWorkDir(workDir); + command.addClasspath("lib/*.jar"); + command.addClasspath("conf/*.xml"); + command.addJavaOption("-Xmx128m"); + + assertThat(command.toString()).isNotNull(); + assertThat(command.getClasspath()).containsOnly("lib/*.jar", "conf/*.xml"); + assertThat(command.getJavaOptions()).containsOnly("-Xmx128m", "-Djava.io.tmpdir=" + tempDir.getAbsolutePath()); + assertThat(command.getWorkDir()).isSameAs(workDir); + assertThat(command.getJmxPort()).isEqualTo(1234); + assertThat(command.getClassName()).isEqualTo("org.sonar.ElasticSearch"); + + // copy current env variables + assertThat(command.getEnvVariables().get("JAVA_COMMAND_TEST")).isEqualTo("1000"); + assertThat(command.getEnvVariables().size()).isEqualTo(System.getenv().size() + 1); + } + + @Test + public void test_debug_mode() throws Exception { + JavaCommand command = new JavaCommand("es"); + assertThat(command.isDebugMode()).isFalse(); + + command.addJavaOption("-Xmx512m"); + assertThat(command.isDebugMode()).isFalse(); + + command.addJavaOption("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005"); + assertThat(command.isDebugMode()).isTrue(); + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java new file mode 100644 index 00000000000..80a02cd39c6 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/JavaProcessLauncherTest.java @@ -0,0 +1,43 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.junit.Test; +import org.sonar.process.NetworkUtils; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; + +public class JavaProcessLauncherTest { + + @Test + public void fail_to_launch() throws Exception { + JavaCommand command = new JavaCommand("test").setJmxPort(NetworkUtils.freePort()); + JavaProcessLauncher launcher = new JavaProcessLauncher(new Timeouts()); + try { + // command is not correct (missing options), java.lang.ProcessBuilder#start() + // throws an exception + launcher.launch(command); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Fail to launch test"); + } + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java new file mode 100644 index 00000000000..6f0be0eb5be --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/MonitorTest.java @@ -0,0 +1,427 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import com.github.kevinsawicki.http.HttpRequest; +import org.apache.commons.io.FileUtils; +import org.junit.After; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.Timeout; +import org.sonar.process.NetworkUtils; +import org.sonar.process.State; +import org.sonar.process.SystemExit; + +import java.io.File; +import java.io.IOException; +import java.net.ConnectException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Mockito.mock; + +public class MonitorTest { + + static File testJar; + Monitor monitor; + SystemExit exit = mock(SystemExit.class); + + /** + * Find the JAR file containing the test apps. Classes can't be moved in sonar-process-monitor because + * they require sonar-process dependencies when executed here (sonar-process, commons-*, ...). + */ + @BeforeClass + public static void initTestJar() { + File targetDir = new File("server/sonar-process/target"); + if (!targetDir.exists() || !targetDir.isDirectory()) { + targetDir = new File("../sonar-process/target"); + } + if (!targetDir.exists() || !targetDir.isDirectory()) { + throw new IllegalStateException("target dir of sonar-process module not found. Please build it."); + } + Collection<File> jars = FileUtils.listFiles(targetDir, new String[] {"jar"}, false); + for (File jar : jars) { + if (jar.getName().startsWith("sonar-process-") && jar.getName().endsWith("-test-jar-with-dependencies.jar")) { + testJar = jar; + return; + } + } + throw new IllegalStateException("No sonar-process-*-test-jar-with-dependencies.jar in " + targetDir); + } + + /** + * Safeguard + */ + @Rule + public Timeout globalTimeout = new Timeout(10000); + + /** + * Temporary directory is used to interact with monitored processes, which write in it. + */ + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + /** + * Safeguard + */ + @After + public void tearDown() throws Exception { + try { + if (monitor != null) { + monitor.stop(); + } + } catch (Throwable ignored) { + } + } + + @Test + public void fail_to_start_if_no_commands() throws Exception { + monitor = newDefaultMonitor(); + try { + monitor.start(Collections.<JavaCommand>emptyList()); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("At least one command is required"); + } + } + + @Test + public void fail_to_start_multiple_times() throws Exception { + monitor = newDefaultMonitor(); + monitor.start(Arrays.asList(newStandardProcessCommand())); + boolean failed = false; + try { + monitor.start(Arrays.asList(newStandardProcessCommand())); + } catch (IllegalStateException e) { + failed = e.getMessage().equals("Can not start multiple times"); + } + monitor.stop(); + assertThat(failed); + } + + @Test + public void start_then_stop_gracefully() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient client = new HttpProcessClient("test"); + // blocks until started + monitor.start(Arrays.asList(client.newCommand())); + + assertThat(client.isReady()).isTrue(); + assertThat(client.wasReadyAt()).isLessThanOrEqualTo(System.currentTimeMillis()); + + // blocks until stopped + monitor.stop(); + assertThat(client.isReady()).isFalse(); + assertThat(client.wasGracefullyTerminated()).isTrue(); + assertThat(monitor.getState()).isEqualTo(State.STOPPED); + } + + @Test + public void start_then_stop_sequence_of_commands() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2"); + monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); + + // start p2 when p1 is fully started (ready) + assertThat(p1.isReady()).isTrue(); + assertThat(p2.isReady()).isTrue(); + assertThat(p2.wasStartingAt()).isGreaterThanOrEqualTo(p1.wasReadyAt()); + + monitor.stop(); + + // stop in inverse order + assertThat(p1.isReady()).isFalse(); + assertThat(p2.isReady()).isFalse(); + assertThat(p1.wasGracefullyTerminated()).isTrue(); + assertThat(p2.wasGracefullyTerminated()).isTrue(); + assertThat(p2.wasGracefullyTerminatedAt()).isLessThanOrEqualTo(p1.wasGracefullyTerminatedAt()); + } + + @Test + public void fail_to_connect_to_jmx() throws Exception { + Timeouts timeouts = new Timeouts(); + monitor = new Monitor(new JavaProcessLauncher(timeouts), + new ImpossibleToConnectJmxConnector(), timeouts, exit); + + HttpProcessClient p1 = new HttpProcessClient("p1"); + try { + monitor.start(Arrays.asList(p1.newCommand())); + fail(); + } catch (Exception e) { + // process was correctly launched, but there was a problem with RMI + assertThat(p1.isReady()).isFalse(); + assertThat(p1.wasGracefullyTerminated()).isFalse(); + } + } + + @Test + public void terminate_all_processes_if_monitor_shutdowns() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2"); + monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); + assertThat(p1.isReady()).isTrue(); + assertThat(p2.isReady()).isTrue(); + + // emulate CTRL-C + monitor.getShutdownHook().run(); + monitor.getShutdownHook().join(); + + assertThat(p1.wasGracefullyTerminated()).isTrue(); + assertThat(p2.wasGracefullyTerminated()).isTrue(); + } + + @Test + public void terminate_all_processes_if_one_monitored_process_shutdowns() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2"); + monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); + assertThat(p1.isReady()).isTrue(); + assertThat(p2.isReady()).isTrue(); + + // kill p1 -> waiting for detection by monitor than termination of p2 + p1.kill(); + monitor.awaitTermination(); + + assertThat(p1.isReady()).isFalse(); + assertThat(p2.isReady()).isFalse(); + assertThat(p1.wasGracefullyTerminated()).isFalse(); + assertThat(p2.wasGracefullyTerminated()).isTrue(); + } + + @Test + public void terminate_all_processes_if_one_fails_to_start() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient p1 = new HttpProcessClient("p1"), p2 = new HttpProcessClient("p2", -1, NetworkUtils.freePort()); + try { + monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); + fail(); + } catch (Exception expected) { + assertThat(p1.wasReady()).isTrue(); + assertThat(p2.wasReady()).isFalse(); + assertThat(p1.wasGracefullyTerminated()).isTrue(); + // self "gracefully terminated", even if startup went bad + assertThat(p2.wasGracefullyTerminated()).isTrue(); + } + } + + @Test + public void kill_process_if_fail_to_request_gracefully_termination() throws Exception { + Timeouts timeouts = new Timeouts(); + timeouts.setTerminationTimeout(100L); + monitor = new Monitor(new JavaProcessLauncher(timeouts), + new TerminationFailureRmiConnector(), timeouts, exit); + + HttpProcessClient p1 = new HttpProcessClient("p1"); + monitor.start(Arrays.asList(p1.newCommand())); + assertThat(p1.isReady()).isTrue(); + + monitor.stop(); + assertThat(p1.isReady()).isFalse(); + } + + @Test + public void fail_to_start_if_bad_class_name() throws Exception { + monitor = newDefaultMonitor(); + JavaCommand command = new JavaCommand("test") + .addClasspath(testJar.getAbsolutePath()) + .setClassName("org.sonar.process.test.Unknown") + .setJmxPort(NetworkUtils.freePort()) + .setTempDir(temp.newFolder()); + + try { + monitor.start(Arrays.asList(command)); + fail(); + } catch (Exception e) { + // expected + // TODO improve, too many stacktraces logged + } + } + + @Test + public void terminate_all_if_one_monitored_process_shutdowns() throws Exception { + monitor = newDefaultMonitor(); + HttpProcessClient client = new HttpProcessClient("test"); + // blocks until started + monitor.start(Arrays.asList(client.newCommand())); + assertThat(client.isReady()).isTrue(); + + client.kill(); + assertThat(client.isReady()).isFalse(); + + // does not wait, already terminated + monitor.awaitTermination(); + + // TODO check logs + } + + @Test + public void fail_if_jmx_port_is_not_available() throws Exception { + monitor = newDefaultMonitor(); + // c1 and c2 have same JMX port + int jmxPort = NetworkUtils.freePort(); + HttpProcessClient p1 = new HttpProcessClient("p1", NetworkUtils.freePort(), jmxPort); + HttpProcessClient p2 = new HttpProcessClient("p2", NetworkUtils.freePort(), jmxPort); + try { + monitor.start(Arrays.asList(p1.newCommand(), p2.newCommand())); + fail(); + } catch (Exception expected) { + assertThat(p1.wasReady()).isTrue(); + assertThat(p2.wasReady()).isFalse(); + assertThat(p1.isReady()).isFalse(); + assertThat(p2.isReady()).isFalse(); + } + } + + @Test + public void disable_autokill_on_jvm_debug_mode() throws Exception { + Timeouts timeouts = new Timeouts(); + timeouts.setMonitorPingInterval(10L); + timeouts.setAutokillPingInterval(10L); + timeouts.setAutokillPingTimeout(10L); + CallVerifierJmxConnector jmxConnector = new CallVerifierJmxConnector(); + monitor = new Monitor(new JavaProcessLauncher(timeouts), jmxConnector, timeouts, exit); + + JavaCommand command = newStandardProcessCommand() + .addJavaOption("-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=" + NetworkUtils.freePort()); + monitor.start(Arrays.asList(command)); + + Thread.sleep(20L); + assertThat(jmxConnector.askedPing).isFalse(); + + monitor.stop(); + } + + private Monitor newDefaultMonitor() { + Timeouts timeouts = new Timeouts(); + return new Monitor(new JavaProcessLauncher(timeouts), new RmiJmxConnector(), timeouts, exit); + } + + /** + * Interaction with {@link org.sonar.process.test.HttpProcess} + */ + private class HttpProcessClient { + private final int httpPort; + private final String commandKey; + private final File tempDir; + private int jmxPort; + + private HttpProcessClient(String commandKey) throws IOException { + this(commandKey, NetworkUtils.freePort(), NetworkUtils.freePort()); + } + + /** + * Use httpPort=-1 to make server fail to start + */ + private HttpProcessClient(String commandKey, int httpPort, int jmxPort) throws IOException { + this.commandKey = commandKey; + this.tempDir = temp.newFolder(commandKey); + this.httpPort = httpPort; + this.jmxPort = jmxPort; + } + + JavaCommand newCommand() throws IOException { + return new JavaCommand(commandKey) + .addClasspath(testJar.getAbsolutePath()) + .setClassName("org.sonar.process.test.HttpProcess") + .setJmxPort(jmxPort) + .setArgument("httpPort", String.valueOf(httpPort)) + .setTempDir(tempDir); + } + + /** + * @see org.sonar.process.test.HttpProcess + */ + boolean isReady() { + try { + HttpRequest httpRequest = HttpRequest.get("http://localhost:" + httpPort + "/ping") + .readTimeout(500).connectTimeout(500); + return httpRequest.ok() && httpRequest.body().equals("ping"); + } catch (HttpRequest.HttpRequestException e) { + if (e.getCause() instanceof ConnectException) { + return false; + } + throw new IllegalStateException("Fail to know the process status", e); + } + } + + /** + * @see org.sonar.process.test.HttpProcess + */ + void kill() { + try { + HttpRequest.post("http://localhost:" + httpPort + "/kill") + .readTimeout(500).connectTimeout(500).ok(); + } catch (Exception e) { + // HTTP request can't be fully processed, as web server hardly + // calls "System.exit()" + } + } + + /** + * @see org.sonar.process.test.HttpProcess + */ + boolean wasGracefullyTerminated() { + return fileExists("terminatedAt"); + } + + long wasStartingAt() throws IOException { + return readTimeFromFile("startingAt"); + } + + long wasGracefullyTerminatedAt() throws IOException { + return readTimeFromFile("terminatedAt"); + } + + boolean wasReady() throws IOException { + return fileExists("readyAt"); + } + + long wasReadyAt() throws IOException { + return readTimeFromFile("readyAt"); + } + + private long readTimeFromFile(String filename) throws IOException { + File file = new File(tempDir, filename); + if (file.isFile() && file.exists()) { + return Long.parseLong(FileUtils.readFileToString(file)); + } + throw new IllegalStateException("File does not exist"); + } + + private boolean fileExists(String filename) { + File file = new File(tempDir, filename); + return file.isFile() && file.exists(); + } + } + + private JavaCommand newStandardProcessCommand() throws IOException { + return new JavaCommand("standard") + .addClasspath(testJar.getAbsolutePath()) + .setClassName("org.sonar.process.test.StandardProcess") + .setJmxPort(NetworkUtils.freePort()) + .setTempDir(temp.newFolder()); + } + +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RmiJmxConnectorTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RmiJmxConnectorTest.java new file mode 100644 index 00000000000..c4a0f735d60 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/RmiJmxConnectorTest.java @@ -0,0 +1,56 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.junit.Test; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.sonar.process.ProcessMXBean; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RmiJmxConnectorTest { + + @Test + public void throw_exception_on_timeout() throws Exception { + RmiJmxConnector connector = new RmiJmxConnector(); + ProcessRef ref = mock(ProcessRef.class); + ProcessMXBean mxBean = mock(ProcessMXBean.class); + connector.register(ref, mxBean); + + when(mxBean.isReady()).thenAnswer(new Answer<Boolean>() { + @Override + public Boolean answer(InvocationOnMock invocationOnMock) throws Throwable { + Thread.sleep(Long.MAX_VALUE); + return null; + } + }); + + try { + connector.isReady(ref, 5L); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Fail send JMX request"); + } + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java new file mode 100644 index 00000000000..0f9f5702666 --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TerminationFailureRmiConnector.java @@ -0,0 +1,27 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +public class TerminationFailureRmiConnector extends RmiJmxConnector { + @Override + public void terminate(ProcessRef processRef, long timeoutMs) { + throw new IllegalStateException("Test - fail to send termination request"); + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java new file mode 100644 index 00000000000..620a59929fc --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/TimeoutsTest.java @@ -0,0 +1,56 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.junit.Test; + +import static org.fest.assertions.Assertions.assertThat; + +public class TimeoutsTest { + + @Test + public void test_default_values() throws Exception { + Timeouts timeouts = new Timeouts(); + assertThat(timeouts.getMonitorPingInterval()).isGreaterThan(1000L); + assertThat(timeouts.getAutokillPingInterval()).isGreaterThan(1000L); + assertThat(timeouts.getAutokillPingTimeout()).isGreaterThan(1000L); + assertThat(timeouts.getTerminationTimeout()).isGreaterThan(1000L); + assertThat(timeouts.getJmxConnectionTimeout()).isGreaterThan(1000L); + assertThat(timeouts.getMonitorIsReadyTimeout()).isGreaterThan(1000L); + } + + @Test + public void test_values() throws Exception { + Timeouts timeouts = new Timeouts(); + timeouts.setAutokillPingInterval(1L); + timeouts.setAutokillPingTimeout(2L); + timeouts.setTerminationTimeout(3L); + timeouts.setJmxConnectionTimeout(4L); + timeouts.setMonitorPingInterval(5L); + timeouts.setMonitorIsReadyTimeout(6L); + + assertThat(timeouts.getAutokillPingInterval()).isEqualTo(1L); + assertThat(timeouts.getAutokillPingTimeout()).isEqualTo(2L); + assertThat(timeouts.getTerminationTimeout()).isEqualTo(3L); + assertThat(timeouts.getJmxConnectionTimeout()).isEqualTo(4L); + assertThat(timeouts.getMonitorPingInterval()).isEqualTo(5L); + assertThat(timeouts.getMonitorIsReadyTimeout()).isEqualTo(6L); + } +} diff --git a/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java new file mode 100644 index 00000000000..86f3a47e6ff --- /dev/null +++ b/server/sonar-process-monitor/src/test/java/org/sonar/process/monitor/WatcherThreadTest.java @@ -0,0 +1,52 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.monitor; + +import org.junit.Test; +import org.mockito.Mockito; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class WatcherThreadTest { + + @Test(timeout = 10000L) + public void kill_process_if_watcher_is_interrupted() throws Exception { + ProcessRef ref = mock(ProcessRef.class, Mockito.RETURNS_DEEP_STUBS); + when(ref.getProcess().waitFor()).thenAnswer(new Answer<Object>() { + @Override + public Object answer(InvocationOnMock invocationOnMock) throws Throwable { + Thread.sleep(Long.MAX_VALUE); + return 0; + } + }); + Monitor monitor = mock(Monitor.class); + + WatcherThread watcher = new WatcherThread(ref, monitor); + watcher.start(); + Thread.sleep(50L); + watcher.interrupt(); + + verify(ref).hardKill(); + } +} diff --git a/server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt index 65b98c522da..65b98c522da 100644 --- a/server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt diff --git a/server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt index b33e179e5c8..b33e179e5c8 100644 --- a/server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt diff --git a/server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt index ab83e4adc03..ab83e4adc03 100644 --- a/server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt diff --git a/server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt index 23f5ecf5104..23f5ecf5104 100644 --- a/server/process/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt diff --git a/server/process/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml b/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml index 298193e01fa..298193e01fa 100644 --- a/server/process/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml diff --git a/server/process/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties b/server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties index 1577a214b3b..1577a214b3b 100644 --- a/server/process/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/ProcessTest/sonar.properties diff --git a/server/process/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties b/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties index 5c06e58a32e..5c06e58a32e 100644 --- a/server/process/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties +++ b/server/sonar-process-monitor/src/test/resources/org/sonar/process/PropsTest/sonar.properties diff --git a/server/process/sonar-process/src/test/resources/sonar-dummy-app.jar b/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar Binary files differindex 6dfd458329a..6dfd458329a 100644 --- a/server/process/sonar-process/src/test/resources/sonar-dummy-app.jar +++ b/server/sonar-process-monitor/src/test/resources/sonar-dummy-app.jar diff --git a/server/process/sonar-process/pom.xml b/server/sonar-process/pom.xml index 9d812c9d868..25413fd7ce9 100644 --- a/server/process/sonar-process/pom.xml +++ b/server/sonar-process/pom.xml @@ -4,14 +4,13 @@ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.codehaus.sonar</groupId> - <artifactId>process</artifactId> - <version>5.0-SNAPSHOT</version> + <artifactId>server</artifactId> + <version>4.5-SNAPSHOT</version> <relativePath>../</relativePath> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>sonar-process</artifactId> - <packaging>jar</packaging> <name>SonarQube :: Process</name> <dependencies> @@ -68,37 +67,42 @@ <artifactId>guava</artifactId> <scope>test</scope> </dependency> + <dependency> + <groupId>org.eclipse.jetty</groupId> + <artifactId>jetty-server</artifactId> + <scope>test</scope> + </dependency> </dependencies> - <!-- <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> - <artifactId>maven-dependency-plugin</artifactId> - <version>2.8</version> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <execution> + <goals> + <goal>test-jar</goal> + </goals> + </execution> + </executions> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-assembly-plugin</artifactId> + <configuration> + <descriptors> + <descriptor>test-jar-with-dependencies.xml</descriptor> + </descriptors> + </configuration> <executions> <execution> - <id>copy</id> - <phase>process-test-resources</phase> + <phase>package</phase> <goals> - <goal>copy</goal> + <goal>single</goal> </goals> - <configuration> - <artifactItems> - <artifactItem> - <groupId>org.codehaus.sonar</groupId> - <artifactId>sonar-dummy-app</artifactId> - <version>${project.version}</version> - <type>jar</type> - <outputDirectory>${project.build.testOutputDirectory}</outputDirectory> - <destFileName>sonar-dummy-app.jar</destFileName> - </artifactItem> - </artifactItems> - </configuration> </execution> </executions> </plugin> </plugins> </build> - --> </project> diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/AesCipher.java b/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java index 204ae1a6b6b..204ae1a6b6b 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/AesCipher.java +++ b/server/sonar-process/src/main/java/org/sonar/process/AesCipher.java diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java b/server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java index 5eb3eecd541..5eb3eecd541 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java +++ b/server/sonar-process/src/main/java/org/sonar/process/Base64Cipher.java diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Cipher.java b/server/sonar-process/src/main/java/org/sonar/process/Cipher.java index 4c437057757..4c437057757 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/Cipher.java +++ b/server/sonar-process/src/main/java/org/sonar/process/Cipher.java diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java b/server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java index 355b6f885a2..b4f86457555 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ConfigurationUtils.java @@ -47,7 +47,7 @@ public final class ConfigurationUtils { return result; } - public static Props loadPropsFromCommandLineArgs(String[] args) { + static Props loadPropsFromCommandLineArgs(String[] args) { if (args.length != 1) { throw new IllegalArgumentException("Only a single command-line argument is accepted " + "(absolute path to configuration file)"); diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Encryption.java b/server/sonar-process/src/main/java/org/sonar/process/Encryption.java index cca05e6c780..cca05e6c780 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/Encryption.java +++ b/server/sonar-process/src/main/java/org/sonar/process/Encryption.java diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/JmxUtils.java b/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java index 58ffb3af3e0..c0cf02bcc89 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/JmxUtils.java +++ b/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java @@ -52,11 +52,12 @@ public class JmxUtils { public static void registerMBean(Object mbean, String name) { try { MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); + ObjectName oName = objectName(name); // Check if already registered in JVM (might run multiple instance in JUnits) - if (mbeanServer.isRegistered(objectName(name))) { - mbeanServer.unregisterMBean(objectName(name)); + if (mbeanServer.isRegistered(oName)) { + mbeanServer.unregisterMBean(oName); } - mbeanServer.registerMBean(mbean, objectName(name)); + mbeanServer.registerMBean(mbean, oName); } catch (RuntimeException re) { throw re; } catch (Exception e) { diff --git a/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java b/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java new file mode 100644 index 00000000000..a492ac751ca --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/Lifecycle.java @@ -0,0 +1,56 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +public class Lifecycle { + + private State state = State.INIT; + + public State getState() { + return state; + } + + public synchronized boolean tryToMoveTo(State to) { + if (state.ordinal() < to.ordinal()) { + state = to; + return true; + } + return false; + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + Lifecycle lifecycle = (Lifecycle) o; + return state == lifecycle.state; + } + + @Override + public int hashCode() { + return state.hashCode(); + } +} diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java b/server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java index 17366850be3..2302a626d2c 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java +++ b/server/sonar-process/src/main/java/org/sonar/process/LoopbackAddress.java @@ -34,9 +34,9 @@ public class LoopbackAddress { } /** - * Quite similar to InetAddress.getLoopbackAddress() which was introduced in Java 7. This + * Quite similar to {@code InetAddress.getLoopbackAddress()} which was introduced in Java 7. This * method aims to support Java 6. It returns an IPv4 address, but not IPv6 in order to - * support -Djava.net.preferIPv4Stack=true recommended for Elasticsearch + * support {@code -Djava.net.preferIPv4Stack=true} which is recommended for Elasticsearch. */ public static InetAddress get() { if (instance == null) { diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/MessageException.java b/server/sonar-process/src/main/java/org/sonar/process/MessageException.java index 5b86ef66c64..5b86ef66c64 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/MessageException.java +++ b/server/sonar-process/src/main/java/org/sonar/process/MessageException.java diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java b/server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java index 2389fa5aa18..2389fa5aa18 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java +++ b/server/sonar-process/src/main/java/org/sonar/process/MinimumViableSystem.java diff --git a/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java b/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java new file mode 100644 index 00000000000..11ddf54beec --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java @@ -0,0 +1,41 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +public interface MonitoredProcess extends Terminable { + + /** + * Starts process. No need to block until fully started and operational. + */ + void start(); + + /** + * True if the process is started and operational (-> can accept requests), false if + * it's still starting. An exception is thrown is process failed to start (not starting + * nor started). + */ + boolean isReady(); + + /** + * Blocks until the process is terminated + */ + void awaitTermination(); + +} diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java b/server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java index 516c57c497a..074cb8cf5c2 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java +++ b/server/sonar-process/src/main/java/org/sonar/process/NetworkUtils.java @@ -19,7 +19,6 @@ */ package org.sonar.process; -import java.io.IOException; import java.net.ServerSocket; public class NetworkUtils { @@ -34,7 +33,7 @@ public class NetworkUtils { int port = s.getLocalPort(); s.close(); return port; - } catch (IOException e) { + } catch (Exception e) { throw new IllegalStateException("Can not find an open network port", e); } } diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java new file mode 100644 index 00000000000..7137a41fa12 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessEntryPoint.java @@ -0,0 +1,155 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +public class ProcessEntryPoint implements ProcessMXBean { + + public static final String PROPERTY_PROCESS_KEY = "process.key"; + public static final String PROPERTY_AUTOKILL_DISABLED = "process.autokill.disabled"; + public static final String PROPERTY_AUTOKILL_PING_TIMEOUT = "process.autokill.pingTimeout"; + public static final String PROPERTY_AUTOKILL_PING_INTERVAL = "process.autokill.pingInterval"; + public static final String PROPERTY_TERMINATION_TIMEOUT = "process.terminationTimeout"; + + private final Props props; + private final Lifecycle lifecycle = new Lifecycle(); + private volatile MonitoredProcess monitoredProcess; + private volatile long lastPing = 0L; + private volatile StopperThread stopperThread; + private final SystemExit exit; + private Thread shutdownHook = new Thread(new Runnable() { + @Override + public void run() { + exit.setInShutdownHook(); + terminate(); + } + }); + + ProcessEntryPoint(Props props, SystemExit exit) { + this.props = props; + this.exit = exit; + } + + public Props getProps() { + return props; + } + + /** + * Launch process and waits until it's down + */ + public void launch(MonitoredProcess mp) { + if (!lifecycle.tryToMoveTo(State.STARTING)) { + throw new IllegalStateException("Already started"); + } + monitoredProcess = mp; + + // TODO check if these properties are available in System Info + JmxUtils.registerMBean(this, props.nonNullValue(PROPERTY_PROCESS_KEY)); + Runtime.getRuntime().addShutdownHook(shutdownHook); + if (!props.valueAsBoolean(PROPERTY_AUTOKILL_DISABLED, false)) { + // mainly for Java Debugger + scheduleAutokill(); + } + + try { + monitoredProcess.start(); + boolean ready = false; + while (!ready) { + ready = monitoredProcess.isReady(); + Thread.sleep(200L); + } + if (lifecycle.tryToMoveTo(State.STARTED)) { + monitoredProcess.awaitTermination(); + } + } catch (Exception e) { + LoggerFactory.getLogger(getClass()).warn("Fail to start", e); + + } finally { + terminate(); + } + } + + @Override + public boolean isReady() { + return lifecycle.getState() == State.STARTED; + } + + @Override + public void ping() { + lastPing = System.currentTimeMillis(); + } + + /** + * Blocks until stopped in a timely fashion (see {@link org.sonar.process.StopperThread}) + */ + @Override + public void terminate() { + if (lifecycle.tryToMoveTo(State.STOPPING)) { + stopperThread = new StopperThread(monitoredProcess, Long.parseLong(props.nonNullValue(PROPERTY_TERMINATION_TIMEOUT))); + stopperThread.start(); + } + try { + // stopperThread is not null for sure + // join() does nothing if thread already finished + stopperThread.join(); + lifecycle.tryToMoveTo(State.STOPPED); + } catch (InterruptedException e) { + // nothing to do, the process is going to be exited + } + exit.exit(0); + } + + private void scheduleAutokill() { + final long autokillPingTimeoutMs = props.valueAsInt(PROPERTY_AUTOKILL_PING_TIMEOUT); + long autokillPingIntervalMs = props.valueAsInt(PROPERTY_AUTOKILL_PING_INTERVAL); + Runnable autokiller = new Runnable() { + @Override + public void run() { + long time = System.currentTimeMillis(); + if (time - lastPing > autokillPingTimeoutMs) { + LoggerFactory.getLogger(getClass()).info(String.format( + "Did not receive any ping during %d seconds. Shutting down.", autokillPingTimeoutMs / 1000)); + terminate(); + } + } + }; + lastPing = System.currentTimeMillis(); + ScheduledExecutorService monitor = Executors.newScheduledThreadPool(1); + monitor.scheduleWithFixedDelay(autokiller, autokillPingIntervalMs, autokillPingIntervalMs, TimeUnit.MILLISECONDS); + } + + State getState() { + return lifecycle.getState(); + } + + Thread getShutdownHook() { + return shutdownHook; + } + + public static ProcessEntryPoint createForArguments(String[] args) { + Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); + return new ProcessEntryPoint(props, new SystemExit()); + } +} diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java index dacddd91847..dacddd91847 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessLogging.java diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java index d212c08ccdd..3d024d420eb 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java @@ -23,5 +23,6 @@ public interface ProcessMXBean extends Terminable { boolean isReady(); - long ping(); + void ping(); + } diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java index f399318aa1e..bdefa116949 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java @@ -20,52 +20,51 @@ package org.sonar.process; import org.apache.commons.io.IOUtils; -import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.annotation.Nullable; -import java.lang.management.ManagementFactory; - public class ProcessUtils { - private static final Logger LOGGER = LoggerFactory.getLogger(ProcessUtils.class); - private ProcessUtils() { // only static stuff } + /** + * Do not abuse to this method. It uses exceptions to get status. + * @return false if process is null or terminated, else true. + */ public static boolean isAlive(@Nullable Process process) { - if (process == null) { - return false; - } - try { - process.exitValue(); - return false; - } catch (IllegalThreadStateException e) { - LOGGER.trace("Process has no exit value yet", e); - return true; + boolean alive = false; + if (process != null) { + try { + process.exitValue(); + } catch (IllegalThreadStateException ignored) { + alive = true; + } } + return alive; } - public static void destroyQuietly(@Nullable Process process) { - if (process != null && isAlive(process)) { + /** + * Destroys process (equivalent to kill -9) if alive + * @return true if the process was destroyed, false if process is null or already destroyed. + */ + public static boolean destroyQuietly(@Nullable Process process) { + boolean destroyed = false; + if (isAlive(process)) { try { process.destroy(); - } catch (Exception ignored) { - LOGGER.warn("Exception while destroying the process", ignored); + while (isAlive(process)) { + // destroy() sends the signal, it does not wait for the process to be down + Thread.sleep(100L); + } + destroyed = true; + } catch (Exception e) { + LoggerFactory.getLogger(ProcessUtils.class).error("Fail to destroy " + process); } } - } - - public static void addSelfShutdownHook(final Terminable terminable) { - Thread shutdownHook = new Thread(new Runnable() { - @Override - public void run() { - terminable.terminate(); - } - }); - Runtime.getRuntime().addShutdownHook(shutdownHook); + return destroyed; } public static void closeStreams(@Nullable Process process) { @@ -75,8 +74,4 @@ public class ProcessUtils { IOUtils.closeQuietly(process.getErrorStream()); } } - - public static boolean isJvmDebugEnabled() { - return ManagementFactory.getRuntimeMXBean().getInputArguments().toString().indexOf("-agentlib:jdwp") > 0; - } } diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Props.java b/server/sonar-process/src/main/java/org/sonar/process/Props.java index b868702eafc..b868702eafc 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/Props.java +++ b/server/sonar-process/src/main/java/org/sonar/process/Props.java diff --git a/server/sonar-process/src/main/java/org/sonar/process/State.java b/server/sonar-process/src/main/java/org/sonar/process/State.java new file mode 100644 index 00000000000..9d773d2bc23 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/State.java @@ -0,0 +1,26 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +public enum State { + + INIT, STARTING, STARTED, STOPPING, STOPPED + +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/StopperThread.java b/server/sonar-process/src/main/java/org/sonar/process/StopperThread.java new file mode 100644 index 00000000000..2d0c6734b30 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/StopperThread.java @@ -0,0 +1,57 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * Gracefully stops process, but exits JVM if too long + */ +class StopperThread extends Thread { + + private final Terminable terminable; + private final long terminationTimeout; + + StopperThread(Terminable terminable, long terminationTimeout) { + super("Stopper"); + this.terminable = terminable; + this.terminationTimeout = terminationTimeout; + } + + @Override + public void run() { + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(new Runnable() { + @Override + public void run() { + terminable.terminate(); + } + }); + try { + future.get(terminationTimeout, TimeUnit.MILLISECONDS); + } catch (Exception e) { + future.cancel(true); + executor.shutdownNow(); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/SystemExit.java b/server/sonar-process/src/main/java/org/sonar/process/SystemExit.java new file mode 100644 index 00000000000..8f12a210633 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/SystemExit.java @@ -0,0 +1,52 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * Calls {@link System#exit(int)} except from shutdown hooks, to prevent + * deadlocks. See http://stackoverflow.com/a/19552359/229031 + */ +public class SystemExit { + + private final AtomicBoolean inShutdownHook = new AtomicBoolean(false); + + public void exit(int code) { + if (!inShutdownHook.get()) { + doExit(code); + } + } + + public boolean isInShutdownHook() { + return inShutdownHook.get(); + } + + /** + * Declarative approach. I don't know how to get this lifecycle state from Java API. + */ + public void setInShutdownHook() { + inShutdownHook.set(true); + } + + void doExit(int code) { + System.exit(code); + } +} diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/Terminable.java b/server/sonar-process/src/main/java/org/sonar/process/Terminable.java index 109e91c42da..a8670609fe2 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/Terminable.java +++ b/server/sonar-process/src/main/java/org/sonar/process/Terminable.java @@ -23,8 +23,6 @@ package org.sonar.process; * This term "terminate" is used in order to not conflict with {@link Thread#stop()}. */ public interface Terminable { - /** - * Stops pending work. Must <b>not</b> throw an exception on error. - */ + void terminate(); } diff --git a/server/process/sonar-process/src/main/java/org/sonar/process/package-info.java b/server/sonar-process/src/main/java/org/sonar/process/package-info.java index 09da5ce266d..09da5ce266d 100644 --- a/server/process/sonar-process/src/main/java/org/sonar/process/package-info.java +++ b/server/sonar-process/src/main/java/org/sonar/process/package-info.java diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java b/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java index 8350eafaa3e..8350eafaa3e 100644 --- a/server/process/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/AesCipherTest.java diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java b/server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java index 2045cd4516d..2045cd4516d 100644 --- a/server/process/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/BaseProcessTest.java diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java index de928b93850..de928b93850 100644 --- a/server/process/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/ConfigurationUtilsTest.java diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java b/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java index 0c11856b0fa..0c11856b0fa 100644 --- a/server/process/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/EncryptionTest.java diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java index d2ab21157ef..599ea5d7a30 100644 --- a/server/process/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/JmxUtilsTest.java @@ -40,19 +40,20 @@ public class JmxUtilsTest { class MyBean implements ProcessMXBean { + @Override - public boolean isReady() { - return false; + public void terminate() { + } @Override - public long ping() { - return 0; + public void ping() { + } @Override - public void terminate() { - + public boolean isReady() { + return true; } } @@ -77,7 +78,6 @@ public class JmxUtilsTest { @Test public void testRegisterMBean() throws Exception { - // 0 Get mbServer and create out test MXBean MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); MyBean mxBean = new MyBean(); @@ -87,11 +87,6 @@ public class JmxUtilsTest { assertThat(mbeanServer.isRegistered(objectName)).isFalse(); JmxUtils.registerMBean(mxBean, mxBean.getClass().getSimpleName()); assertThat(mbeanServer.isRegistered(objectName)).isTrue(); - - // 2 assert that we can over-register - assertThat(mbeanServer.isRegistered(objectName)).isTrue(); - JmxUtils.registerMBean(mxBean, mxBean.getClass().getSimpleName()); - assertThat(mbeanServer.isRegistered(objectName)).isTrue(); } @Test diff --git a/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java b/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java new file mode 100644 index 00000000000..23886125212 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/LifecycleTest.java @@ -0,0 +1,42 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.junit.Test; + +import static org.fest.assertions.Assertions.assertThat; + +public class LifecycleTest { + + @Test + public void equals_and_hashcode() throws Exception { + Lifecycle init = new Lifecycle(); + assertThat(init.equals(init)).isTrue(); + assertThat(init.equals(new Lifecycle())).isTrue(); + assertThat(init.equals("INIT")).isFalse(); + assertThat(init.equals(null)).isFalse(); + assertThat(init.hashCode()).isEqualTo(new Lifecycle().hashCode()); + + // different state + Lifecycle stopping = new Lifecycle(); + stopping.tryToMoveTo(State.STOPPING); + assertThat(stopping).isNotEqualTo(init); + } +} diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java b/server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java index 6a8819c3a81..6a8819c3a81 100644 --- a/server/process/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/LoopbackAddressTest.java diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java b/server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java index 83841c79952..83841c79952 100644 --- a/server/process/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/MinimumViableSystemTest.java diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java index 09f6a597209..09f6a597209 100644 --- a/server/process/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/NetworkUtilsTest.java diff --git a/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java new file mode 100644 index 00000000000..03cd7409a49 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/ProcessEntryPointTest.java @@ -0,0 +1,234 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.apache.commons.io.FileUtils; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.junit.rules.Timeout; +import org.sonar.process.test.StandardProcess; + +import java.io.File; +import java.util.Properties; + +import static org.fest.assertions.Assertions.assertThat; +import static org.fest.assertions.Fail.fail; +import static org.mockito.Mockito.mock; + +public class ProcessEntryPointTest { + + SystemExit exit = mock(SystemExit.class); + + /** + * Safeguard + */ + @Rule + public Timeout timeout = new Timeout(10000); + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + @Test + public void load_properties_from_file() throws Exception { + File propsFile = temp.newFile(); + FileUtils.write(propsFile, "sonar.foo=bar"); + + ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(new String[]{propsFile.getAbsolutePath()}); + assertThat(entryPoint.getProps().value("sonar.foo")).isEqualTo("bar"); + } + + @Test + public void test_initial_state() throws Exception { + Props props = new Props(new Properties()); + ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + + assertThat(entryPoint.getProps()).isSameAs(props); + assertThat(entryPoint.isReady()).isFalse(); + assertThat(entryPoint.getState()).isEqualTo(State.INIT); + + // do not fail + entryPoint.ping(); + } + + @Test + public void fail_to_launch_if_missing_monitor_properties() throws Exception { + Props props = new Props(new Properties()); + ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + + StandardProcess process = new StandardProcess(); + try { + entryPoint.launch(process); + fail(); + } catch (IllegalArgumentException e) { + assertThat(e).hasMessage("Missing property: process.key"); + assertThat(process.getState()).isEqualTo(State.INIT); + } + } + + @Test + public void fail_to_launch_multiple_times() throws Exception { + Props props = new Props(new Properties()); + props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); + props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); + ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + + entryPoint.launch(new NoopProcess()); + try { + entryPoint.launch(new NoopProcess()); + fail(); + } catch (IllegalStateException e) { + assertThat(e).hasMessage("Already started"); + } + } + + @Test + public void launch_then_request_graceful_termination() throws Exception { + Props props = new Props(new Properties()); + props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); + props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); + final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + final StandardProcess process = new StandardProcess(); + + Thread runner = new Thread() { + @Override + public void run() { + // starts and waits until terminated + entryPoint.launch(process); + } + }; + runner.start(); + + while (process.getState() != State.STARTED) { + Thread.sleep(10L); + } + + // requests for termination -> waits until down + // Should terminate before the timeout of 30s + entryPoint.terminate(); + + assertThat(process.getState()).isEqualTo(State.STOPPED); + } + + @Test + public void autokill_if_no_pings() throws Exception { + Props props = new Props(new Properties()); + props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "test"); + props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_INTERVAL, "5"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_PING_TIMEOUT, "1"); + final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + final StandardProcess process = new StandardProcess(); + + entryPoint.launch(process); + + assertThat(process.getState()).isEqualTo(State.STOPPED); + } + + @Test + public void terminate_if_unexpected_shutdown() throws Exception { + Props props = new Props(new Properties()); + props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); + props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); + final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + final StandardProcess process = new StandardProcess(); + + Thread runner = new Thread() { + @Override + public void run() { + // starts and waits until terminated + entryPoint.launch(process); + } + }; + runner.start(); + while (process.getState() != State.STARTED) { + Thread.sleep(10L); + } + + // emulate signal to shutdown process + entryPoint.getShutdownHook().start(); + while (process.getState() != State.STOPPED) { + Thread.sleep(10L); + } + // exit before test timeout, ok ! + } + + @Test + public void terminate_if_startup_error() throws Exception { + Props props = new Props(new Properties()); + props.set(ProcessEntryPoint.PROPERTY_PROCESS_KEY, "foo"); + props.set(ProcessEntryPoint.PROPERTY_AUTOKILL_DISABLED, "true"); + props.set(ProcessEntryPoint.PROPERTY_TERMINATION_TIMEOUT, "30000"); + final ProcessEntryPoint entryPoint = new ProcessEntryPoint(props, exit); + final MonitoredProcess process = new StartupErrorProcess(); + + entryPoint.launch(process); + assertThat(entryPoint.getState()).isEqualTo(State.STOPPED); + } + + private static class NoopProcess implements MonitoredProcess { + + @Override + public void start() { + + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void awaitTermination() { + + } + + @Override + public void terminate() { + + } + } + + private static class StartupErrorProcess implements MonitoredProcess { + + @Override + public void start() { + throw new IllegalStateException("ERROR"); + } + + @Override + public boolean isReady() { + return false; + } + + @Override + public void awaitTermination() { + + } + + @Override + public void terminate() { + + } + } +} diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java b/server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java index e508f5a57f9..6f93126516e 100644 --- a/server/process/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/ProcessUtilsTest.java @@ -25,8 +25,4 @@ import static org.fest.assertions.Assertions.assertThat; public class ProcessUtilsTest { - @Test - public void isJvmDebugEnabled() { - assertThat(ProcessUtils.isJvmDebugEnabled()).isFalse(); - } } diff --git a/server/process/sonar-process/src/test/java/org/sonar/process/PropsTest.java b/server/sonar-process/src/test/java/org/sonar/process/PropsTest.java index 6985b0ba086..5d283b44f8f 100644 --- a/server/process/sonar-process/src/test/java/org/sonar/process/PropsTest.java +++ b/server/sonar-process/src/test/java/org/sonar/process/PropsTest.java @@ -29,7 +29,7 @@ import static org.fest.assertions.Fail.fail; public class PropsTest { @Test - public void value() throws Exception { + public void of() throws Exception { Properties p = new Properties(); p.setProperty("foo", "bar"); Props props = new Props(p); @@ -41,7 +41,7 @@ public class PropsTest { } @Test - public void valueAsInt() throws Exception { + public void intOf() throws Exception { Properties p = new Properties(); p.setProperty("foo", "33"); p.setProperty("blank", ""); @@ -56,7 +56,7 @@ public class PropsTest { } @Test - public void valueAsInt_not_integer() throws Exception { + public void intOf_not_integer() throws Exception { Properties p = new Properties(); p.setProperty("foo", "bar"); Props props = new Props(p); @@ -70,7 +70,7 @@ public class PropsTest { } @Test - public void valueAsBoolean() throws Exception { + public void booleanOf() throws Exception { Properties p = new Properties(); p.setProperty("foo", "True"); p.setProperty("bar", "false"); @@ -82,7 +82,7 @@ public class PropsTest { } @Test - public void valueAsBoolean_default_value() throws Exception { + public void booleanOf_default_value() throws Exception { Properties p = new Properties(); p.setProperty("foo", "true"); p.setProperty("bar", "false"); @@ -114,11 +114,9 @@ public class PropsTest { Props props = new Props(p); props.set("foo", "new_foo"); props.set("bar", "new_bar"); - props.set("null", null); assertThat(props.value("foo")).isEqualTo("new_foo"); assertThat(props.value("bar")).isEqualTo("new_bar"); - assertThat(props.value("null")).isNull(); } @Test diff --git a/server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java b/server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java new file mode 100644 index 00000000000..e02b07cf555 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/SystemExitTest.java @@ -0,0 +1,56 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.junit.Test; + +import java.util.concurrent.atomic.AtomicInteger; + +import static org.fest.assertions.Assertions.assertThat; + +public class SystemExitTest { + + @Test + public void do_not_exit_if_in_shutdown_hook() throws Exception { + SystemExit systemExit = new SystemExit(); + + systemExit.setInShutdownHook(); + assertThat(systemExit.isInShutdownHook()).isTrue(); + + systemExit.exit(0); + // still there + } + + @Test + public void exit_if_not_in_shutdown_hook() throws Exception { + final AtomicInteger got = new AtomicInteger(); + SystemExit systemExit = new SystemExit() { + @Override + void doExit(int code) { + got.set(code); + } + }; + + assertThat(systemExit.isInShutdownHook()).isFalse(); + systemExit.exit(1); + + assertThat(got.get()).isEqualTo(1); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java b/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java new file mode 100644 index 00000000000..f6ca587a588 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/test/HttpProcess.java @@ -0,0 +1,124 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.test; + +import org.apache.commons.io.FileUtils; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.AbstractHandler; +import org.eclipse.jetty.server.handler.ContextHandler; +import org.sonar.process.MonitoredProcess; +import org.sonar.process.ProcessEntryPoint; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import java.io.File; +import java.io.IOException; + +/** + * Http server used for testing (see MonitorTest). It accepts HTTP commands /ping and /kill to hardly exit. + * It also pushes status to temp files, so test can verify what was really done (when server went ready state and + * if it was gracefully terminated) + */ +public class HttpProcess implements MonitoredProcess { + + private final Server server; + private boolean ready = false; + // temp dir is specific to this process + private final File tempDir = new File(System.getProperty("java.io.tmpdir")); + + public HttpProcess(int httpPort) { + server = new Server(httpPort); + } + + @Override + public void start() { + writeTimeToFile("startingAt"); + ContextHandler context = new ContextHandler(); + context.setContextPath("/"); + context.setClassLoader(Thread.currentThread().getContextClassLoader()); + server.setHandler(context); + context.setHandler(new AbstractHandler() { + @Override + public void handle(String target, Request request, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws IOException, ServletException { + if ("/ping".equals(target)) { + request.setHandled(true); + httpServletResponse.getWriter().print("ping"); + } else if ("/kill".equals(target)) { + writeTimeToFile("killedAt"); + System.exit(0); + } + } + }); + try { + server.start(); + } catch (Exception e) { + throw new IllegalStateException("Fail to start Jetty", e); + } + } + + @Override + public boolean isReady() { + if (ready) { + return true; + } + if (server.isStarted()) { + ready = true; + writeTimeToFile("readyAt"); + } + return false; + } + + @Override + public void awaitTermination() { + try { + server.join(); + } catch (InterruptedException ignore) { + + } + } + + @Override + public void terminate() { + try { + if (!server.isStopped()) { + server.stop(); + writeTimeToFile("terminatedAt"); + } + } catch (Exception e) { + throw new IllegalStateException("Fail to stop Jetty", e); + } + } + + private void writeTimeToFile(String filename) { + try { + FileUtils.write(new File(tempDir, filename), String.valueOf(System.currentTimeMillis())); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public static void main(String[] args) { + ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); + entryPoint.launch(new HttpProcess(entryPoint.getProps().valueAsInt("httpPort"))); + } +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java b/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java new file mode 100644 index 00000000000..4c1a33221c6 --- /dev/null +++ b/server/sonar-process/src/test/java/org/sonar/process/test/StandardProcess.java @@ -0,0 +1,86 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process.test; + +import org.sonar.process.MonitoredProcess; +import org.sonar.process.ProcessEntryPoint; +import org.sonar.process.State; + +public class StandardProcess implements MonitoredProcess { + + private State state = State.INIT; + + private final Thread daemon = new Thread() { + @Override + public void run() { + try { + while (true) { + Thread.sleep(100L); + } + } catch (InterruptedException e) { + // return + } + } + }; + + /** + * Blocks until started() + */ + @Override + public void start() { + state = State.STARTING; + daemon.start(); + state = State.STARTED; + } + + @Override + public boolean isReady() { + return state == State.STARTED; + } + + @Override + public void awaitTermination() { + try { + daemon.join(); + } catch (InterruptedException e) { + // interrupted by call to terminate() + } + } + + /** + * Blocks until stopped + */ + @Override + public void terminate() { + state = State.STOPPING; + daemon.interrupt(); + state = State.STOPPED; + } + + public State getState() { + return state; + } + + public static void main(String[] args) { + ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); + entryPoint.launch(new StandardProcess()); + System.exit(0); + } +} diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt new file mode 100644 index 00000000000..65b98c522da --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/aes_secret_key.txt @@ -0,0 +1 @@ +0PZz+G+f8mjr3sPn4+AhHg==
\ No newline at end of file diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt new file mode 100644 index 00000000000..b33e179e5c8 --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/bad_secret_key.txt @@ -0,0 +1 @@ +badbadbad==
\ No newline at end of file diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt new file mode 100644 index 00000000000..ab83e4adc03 --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/non_trimmed_secret_key.txt @@ -0,0 +1,3 @@ + + 0PZz+G+f8mjr3sPn4+AhHg== + diff --git a/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt new file mode 100644 index 00000000000..23f5ecf5104 --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/AesCipherTest/other_secret_key.txt @@ -0,0 +1 @@ +IBxEUxZ41c8XTxyaah1Qlg==
\ No newline at end of file diff --git a/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml b/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml new file mode 100644 index 00000000000..298193e01fa --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/LoggingTest/logback-access.xml @@ -0,0 +1 @@ +<configuration/> diff --git a/server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties b/server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties new file mode 100644 index 00000000000..1577a214b3b --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/ProcessTest/sonar.properties @@ -0,0 +1,212 @@ +# This file must contain only ISO 8859-1 characters +# see http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Properties.html#load(java.io.InputStream) +# +# To use an environment variable, use the following syntax : ${env:NAME_OF_ENV_VARIABLE} +# For example: +# sonar.jdbc.url= ${env:SONAR_JDBC_URL} +# +# +# See also the file conf/wrapper.conf for JVM advanced settings + + + +#-------------------------------------------------------------------------------------------------- +# DATABASE +# +# IMPORTANT: the embedded H2 database is used by default. It is recommended for tests only. +# Please use a production-ready database. Supported databases are MySQL, Oracle, PostgreSQL +# and Microsoft SQLServer. + +# Permissions to create tables, indices and triggers must be granted to JDBC user. +# The schema must be created first. +sonar.jdbc.username=sonar +sonar.jdbc.password=sonar + +#----- Embedded database H2 +# Note: it does not accept connections from remote hosts, so the +# SonarQube server and the maven plugin must be executed on the same host. + +# Comment the following line to deactivate the default embedded database. +sonar.jdbc.url=jdbc:h2:tcp://localhost:9092/sonar + +# directory containing H2 database files. By default it's the /data directory in the SonarQube installation. +#sonar.embeddedDatabase.dataDir= +# H2 embedded database server listening port, defaults to 9092 +#sonar.embeddedDatabase.port=9092 + + +#----- MySQL 5.x +# Comment the embedded database and uncomment the following line to use MySQL +#sonar.jdbc.url=jdbc:mysql://localhost:3306/sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true + + +#----- Oracle 10g/11g +# To connect to Oracle database: +# +# - It's recommended to use the latest version of the JDBC driver (ojdbc6.jar). +# Download it in http://www.oracle.com/technetwork/database/enterprise-edition/jdbc-112010-090769.html +# - Copy the driver to the directory extensions/jdbc-driver/oracle/ +# - If you need to set the schema, please refer to http://jira.codehaus.org/browse/SONAR-5000 +# - Comment the embedded database and uncomment the following line: +#sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE + + +#----- PostgreSQL 8.x/9.x +# Comment the embedded database and uncomment the following property to use PostgreSQL. +# If you don't use the schema named "public", please refer to http://jira.codehaus.org/browse/SONAR-5000 +#sonar.jdbc.url=jdbc:postgresql://localhost/sonar + + +#----- Microsoft SQLServer +# The Jtds open source driver is available in extensions/jdbc-driver/mssql. More details on http://jtds.sourceforge.net +#sonar.jdbc.url=jdbc:jtds:sqlserver://localhost/sonar;SelectMethod=Cursor + + +#----- Connection pool settings +sonar.jdbc.maxActive=20 +sonar.jdbc.maxIdle=5 +sonar.jdbc.minIdle=2 +sonar.jdbc.maxWait=5000 +sonar.jdbc.minEvictableIdleTimeMillis=600000 +sonar.jdbc.timeBetweenEvictionRunsMillis=30000 + + + +#-------------------------------------------------------------------------------------------------- +# WEB SERVER + +# Binding IP address. For servers with more than one IP address, this property specifies which +# address will be used for listening on the specified ports. +# By default, ports will be used on all IP addresses associated with the server. +#sonar.web.host=0.0.0.0 + +# Web context. When set, it must start with forward slash (for example /sonarqube). +# The default value is root context (empty value). +#sonar.web.context= + +# TCP port for incoming HTTP connections. Disabled when value is -1. +#sonar.web.port=9000 + +# TCP port for incoming HTTPS connections. Disabled when value is -1 (default). +#sonar.web.https.port=-1 + +# HTTPS - the alias used to for the server certificate in the keystore. +# If not specified the first key read in the keystore is used. +#sonar.web.https.keyAlias= + +# HTTPS - the password used to access the server certificate from the +# specified keystore file. The default value is "changeit". +#sonar.web.https.keyPass=changeit + +# HTTPS - the pathname of the keystore file where is stored the server certificate. +# By default, the pathname is the file ".keystore" in the user home. +# If keystoreType doesn't need a file use empty value. +#sonar.web.https.keystoreFile= + +# HTTPS - the password used to access the specified keystore file. The default +# value is the value of sonar.web.https.keyPass. +#sonar.web.https.keystorePass= + +# HTTPS - the type of keystore file to be used for the server certificate. +# The default value is JKS (Java KeyStore). +#sonar.web.https.keystoreType=JKS + +# HTTPS - the name of the keystore provider to be used for the server certificate. +# If not specified, the list of registered providers is traversed in preference order +# and the first provider that supports the keystore type is used (see sonar.web.https.keystoreType). +#sonar.web.https.keystoreProvider= + +# HTTPS - the pathname of the truststore file which contains trusted certificate authorities. +# By default, this would be the cacerts file in your JRE. +# If truststoreFile doesn't need a file use empty value. +#sonar.web.https.truststoreFile= + +# HTTPS - the password used to access the specified truststore file. +#sonar.web.https.truststorePass= + +# HTTPS - the type of truststore file to be used. +# The default value is JKS (Java KeyStore). +#sonar.web.https.truststoreType=JKS + +# HTTPS - the name of the truststore provider to be used for the server certificate. +# If not specified, the list of registered providers is traversed in preference order +# and the first provider that supports the truststore type is used (see sonar.web.https.truststoreType). +#sonar.web.https.truststoreProvider= + +# HTTPS - whether to enable client certificate authentication. +# The default is false (client certificates disabled). +# Other possible values are 'want' (certificates will be requested, but not required), +# and 'true' (certificates are required). +#sonar.web.https.clientAuth=false + +# The maximum number of connections that the server will accept and process at any given time. +# When this number has been reached, the server will not accept any more connections until +# the number of connections falls below this value. The operating system may still accept connections +# based on the sonar.web.connections.acceptCount property. The default value is 50 for each +# enabled connector. +#sonar.web.http.maxThreads=50 +#sonar.web.https.maxThreads=50 + +# The minimum number of threads always kept running. The default value is 5 for each +# enabled connector. +#sonar.web.http.minThreads=5 +#sonar.web.https.minThreads=5 + +# The maximum queue length for incoming connection requests when all possible request processing +# threads are in use. Any requests received when the queue is full will be refused. +# The default value is 25 for each enabled connector. +#sonar.web.http.acceptCount=25 +#sonar.web.https.acceptCount=25 + +# Access logs are generated in the file logs/access.log. This file is rolled over when it's 5Mb. +# An archive of 3 files is kept in the same directory. +# Access logs are enabled by default. +#sonar.web.accessLogs.enable=true + +# TCP port for incoming AJP connections. Disabled when value is -1. +# sonar.ajp.port=9009 + + + +#-------------------------------------------------------------------------------------------------- +# UPDATE CENTER + +# The Update Center requires an internet connection to request http://update.sonarsource.org +# It is enabled by default. +#sonar.updatecenter.activate=true + +# HTTP proxy (default none) +#http.proxyHost= +#http.proxyPort= + +# NT domain name if NTLM proxy is used +#http.auth.ntlm.domain= + +# SOCKS proxy (default none) +#socksProxyHost= +#socksProxyPort= + +# proxy authentication. The 2 following properties are used for HTTP and SOCKS proxies. +#http.proxyUser= +#http.proxyPassword= + + +#-------------------------------------------------------------------------------------------------- +# NOTIFICATIONS + +# Delay in seconds between processing of notification queue. Default is 60. +#sonar.notifications.delay=60 + + +#-------------------------------------------------------------------------------------------------- +# PROFILING +# Level of information displayed in the logs: NONE (default), BASIC (functional information) and FULL (functional and technical details) +#sonar.log.profilingLevel=NONE + + +#-------------------------------------------------------------------------------------------------- +# DEVELOPMENT MODE +# Only for debugging + +# Set to true to apply Ruby on Rails code changes on the fly +#sonar.rails.dev=false diff --git a/server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties b/server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties new file mode 100644 index 00000000000..5c06e58a32e --- /dev/null +++ b/server/sonar-process/src/test/resources/org/sonar/process/PropsTest/sonar.properties @@ -0,0 +1,3 @@ +hello: world +foo=bar +java.io.tmpdir=/should/be/overridden diff --git a/server/sonar-process/src/test/resources/sonar-dummy-app.jar b/server/sonar-process/src/test/resources/sonar-dummy-app.jar Binary files differnew file mode 100644 index 00000000000..6dfd458329a --- /dev/null +++ b/server/sonar-process/src/test/resources/sonar-dummy-app.jar diff --git a/server/sonar-process/test-jar-with-dependencies.xml b/server/sonar-process/test-jar-with-dependencies.xml new file mode 100644 index 00000000000..832c66cc0be --- /dev/null +++ b/server/sonar-process/test-jar-with-dependencies.xml @@ -0,0 +1,19 @@ +<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.0 http://maven.apache.org/xsd/assembly-1.1.0.xsd"> + <id>test-jar-with-dependencies</id> + <formats> + <format>jar</format> + </formats> + <includeBaseDirectory>false</includeBaseDirectory> + <dependencySets> + <dependencySet> + <outputDirectory>/</outputDirectory> + <useProjectArtifact>true</useProjectArtifact> + <!-- we're creating the test-jar as an attachement --> + <useProjectAttachments>true</useProjectAttachments> + <unpack>true</unpack> + <scope>test</scope> + </dependencySet> + </dependencySets> +</assembly> diff --git a/server/sonar-search/pom.xml b/server/sonar-search/pom.xml index e44acc5fdf6..f15dd473a3f 100644 --- a/server/sonar-search/pom.xml +++ b/server/sonar-search/pom.xml @@ -1,7 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" - xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <groupId>org.codehaus.sonar</groupId> <artifactId>server</artifactId> diff --git a/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java index 1abf733b66b..d2aa111e7e5 100644 --- a/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java +++ b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java @@ -21,15 +21,14 @@ package org.sonar.search; import org.apache.commons.lang.StringUtils; import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus; -import org.elasticsearch.common.annotations.VisibleForTesting; import org.elasticsearch.common.settings.ImmutableSettings; import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.node.Node; import org.elasticsearch.node.NodeBuilder; import org.slf4j.LoggerFactory; -import org.sonar.process.ConfigurationUtils; import org.sonar.process.MinimumViableSystem; import org.sonar.process.MonitoredProcess; +import org.sonar.process.ProcessEntryPoint; import org.sonar.process.ProcessLogging; import org.sonar.process.Props; import org.sonar.search.script.ListUpdate; @@ -40,7 +39,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; -public class SearchServer extends MonitoredProcess { +public class SearchServer implements MonitoredProcess { public static final String SONAR_NODE_NAME = "sonar.node.name"; public static final String ES_PORT_PROPERTY = "sonar.search.port"; @@ -55,27 +54,13 @@ public class SearchServer extends MonitoredProcess { private static final Integer MINIMUM_INDEX_REPLICATION = 1; private final Set<String> nodes = new HashSet<String>(); - private final boolean isBlocking; - - private Node node; + private final Props props; private final Object lock = new Object(); - @VisibleForTesting - public SearchServer(final Props props, boolean monitored, boolean blocking) { - super(props, monitored); - - this.isBlocking = blocking; - new MinimumViableSystem().check(); - - String esNodesInets = props.value(ES_CLUSTER_INET); - if (StringUtils.isNotEmpty(esNodesInets)) { - Collections.addAll(nodes, esNodesInets.split(",")); - } - } + private Node node; public SearchServer(Props props) { - super(props); - this.isBlocking = true; + this.props = props; new MinimumViableSystem().check(); String esNodesInets = props.value(ES_CLUSTER_INET); @@ -85,18 +70,8 @@ public class SearchServer extends MonitoredProcess { } @Override - protected boolean doIsReady() { - return node.client().admin().cluster().prepareHealth() - .setWaitForYellowStatus() - .setTimeout(TimeValue.timeValueSeconds(3L)) - .get() - .getStatus() != ClusterHealthStatus.RED; - } - - @Override - protected void doStart() { + public void start() { synchronized (lock) { - Integer port = props.valueAsInt(ES_PORT_PROPERTY); String clusterName = props.value(ES_CLUSTER_PROPERTY); @@ -169,17 +144,26 @@ public class SearchServer extends MonitoredProcess { .addMapping("_default_", "{\"dynamic\": \"strict\"}") .get(); } + } - if (isBlocking) { - while (node != null && !node.isClosed()) { - try { - Thread.sleep(100); - } catch (InterruptedException e) { - // Ignore - } + @Override + public boolean isReady() { + return node.client().admin().cluster().prepareHealth() + .setWaitForYellowStatus() + .setTimeout(TimeValue.timeValueSeconds(3L)) + .get() + .getStatus() != ClusterHealthStatus.RED; + } + + @Override + public void awaitTermination() { + while (node != null && !node.isClosed()) { + try { + Thread.sleep(200L); + } catch (InterruptedException e) { + // Ignore } } - } private void initAnalysis(ImmutableSettings.Builder esSettings) { @@ -188,40 +172,40 @@ public class SearchServer extends MonitoredProcess { // Disallow dynamic mapping (too expensive) .put("index.mapper.dynamic", false) - // Sortable text analyzer + // Sortable text analyzer .put("index.analysis.analyzer.sortable.type", "custom") .put("index.analysis.analyzer.sortable.tokenizer", "keyword") .putArray("index.analysis.analyzer.sortable.filter", "trim", "lowercase", "truncate") - // Edge NGram index-analyzer + // Edge NGram index-analyzer .put("index.analysis.analyzer.index_grams.type", "custom") .put("index.analysis.analyzer.index_grams.tokenizer", "whitespace") .putArray("index.analysis.analyzer.index_grams.filter", "trim", "lowercase", "gram_filter") - // Edge NGram search-analyzer + // Edge NGram search-analyzer .put("index.analysis.analyzer.search_grams.type", "custom") .put("index.analysis.analyzer.search_grams.tokenizer", "whitespace") .putArray("index.analysis.analyzer.search_grams.filter", "trim", "lowercase") - // Word index-analyzer + // Word index-analyzer .put("index.analysis.analyzer.index_words.type", "custom") .put("index.analysis.analyzer.index_words.tokenizer", "standard") .putArray("index.analysis.analyzer.index_words.filter", "standard", "word_filter", "lowercase", "stop", "asciifolding", "porter_stem") - // Word search-analyzer + // Word search-analyzer .put("index.analysis.analyzer.search_words.type", "custom") .put("index.analysis.analyzer.search_words.tokenizer", "standard") .putArray("index.analysis.analyzer.search_words.filter", "standard", "lowercase", "stop", "asciifolding", "porter_stem") - // Edge NGram filter + // Edge NGram filter .put("index.analysis.filter.gram_filter.type", "edgeNGram") .put("index.analysis.filter.gram_filter.min_gram", 2) .put("index.analysis.filter.gram_filter.max_gram", 15) .putArray("index.analysis.filter.gram_filter.token_chars", "letter", "digit", "punctuation", "symbol") - // Word filter + // Word filter .put("index.analysis.filter.word_filter.type", "word_delimiter") .put("index.analysis.filter.word_filter.generate_word_parts", true) .put("index.analysis.filter.word_filter.catenate_words", true) @@ -232,7 +216,7 @@ public class SearchServer extends MonitoredProcess { .put("index.analysis.filter.word_filter.split_on_numerics", true) .put("index.analysis.filter.word_filter.stem_english_possessive", true) - // Path Analyzer + // Path Analyzer .put("index.analysis.analyzer.path_analyzer.type", "custom") .put("index.analysis.analyzer.path_analyzer.tokenizer", "path_hierarchy"); @@ -267,18 +251,18 @@ public class SearchServer extends MonitoredProcess { } @Override - protected void doTerminate() { + public void terminate() { synchronized (lock) { - if (node != null && !node.isClosed()) { + if (!node.isClosed()) { node.close(); - node = null; } } } public static void main(String... args) { - Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); - new ProcessLogging().configure(props, "/org/sonar/search/logback.xml"); - new SearchServer(props).start(); + ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); + new ProcessLogging().configure(entryPoint.getProps(), "/org/sonar/search/logback.xml"); + SearchServer searchServer = new SearchServer(entryPoint.getProps()); + entryPoint.launch(searchServer); } } diff --git a/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java b/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java index bdf54bb5187..54fb02435e9 100644 --- a/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java +++ b/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java @@ -26,16 +26,17 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.junit.After; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; import org.sonar.process.JmxUtils; -import org.sonar.process.MonitoredProcess; import org.sonar.process.Props; import javax.management.InstanceNotFoundException; import javax.management.MBeanRegistrationException; import javax.management.MBeanServer; + import java.io.IOException; import java.lang.management.ManagementFactory; import java.net.ServerSocket; @@ -44,6 +45,7 @@ import java.util.Properties; import static org.fest.assertions.Assertions.assertThat; import static org.junit.Assert.fail; +@Ignore public class SearchServerTest { @Rule @@ -81,7 +83,6 @@ public class SearchServerTest { @Test public void server_fail_to_start() throws Exception { Properties properties = new Properties(); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); searchServer = new SearchServer(new Props(properties)); new Thread(new Runnable() { @@ -107,7 +108,6 @@ public class SearchServerTest { @Test public void can_connect() throws Exception { Properties properties = new Properties(); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); properties.setProperty(SearchServer.SONAR_PATH_DATA, temp.newFolder().getAbsolutePath()); properties.setProperty(SearchServer.SONAR_PATH_TEMP, temp.newFolder().getAbsolutePath()); properties.setProperty(SearchServer.SONAR_PATH_LOG, temp.newFolder().getAbsolutePath()); diff --git a/server/sonar-server/pom.xml b/server/sonar-server/pom.xml index 3cf8474f9fe..188888b6367 100644 --- a/server/sonar-server/pom.xml +++ b/server/sonar-server/pom.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.codehaus.sonar</groupId> diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java b/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java index 2356ff340e8..01a5305c53d 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/EmbeddedTomcat.java @@ -19,62 +19,64 @@ */ package org.sonar.server.app; +import com.google.common.base.Throwables; +import org.apache.catalina.LifecycleException; import org.apache.catalina.core.StandardContext; import org.apache.catalina.startup.Tomcat; import org.apache.commons.io.FileUtils; import org.slf4j.LoggerFactory; -import org.sonar.process.ProcessUtils; import org.sonar.process.Props; -import org.sonar.process.Terminable; import java.io.File; -class EmbeddedTomcat implements Terminable { +class EmbeddedTomcat { private final Props props; private Tomcat tomcat = null; - private Thread hook = null; - private boolean ready = false; + private volatile StandardContext webappContext; EmbeddedTomcat(Props props) { this.props = props; } void start() { - if (tomcat != null || hook != null) { - throw new IllegalStateException("Server is already started"); - } - - try { - // '%2F' (slash /) and '%5C' (backslash \) are permitted as path delimiters in URLs - // See Ruby on Rails url_for - System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true"); + // '%2F' (slash /) and '%5C' (backslash \) are permitted as path delimiters in URLs + // See Ruby on Rails url_for + System.setProperty("org.apache.tomcat.util.buf.UDecoder.ALLOW_ENCODED_SLASH", "true"); - System.setProperty("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE", "true"); + System.setProperty("org.apache.catalina.startup.EXIT_ON_INIT_FAILURE", "true"); - tomcat = new Tomcat(); - // Initialize directories - String basedir = tomcatBasedir().getAbsolutePath(); - tomcat.setBaseDir(basedir); - tomcat.getHost().setAppBase(basedir); - tomcat.getHost().setAutoDeploy(false); - tomcat.getHost().setCreateDirs(false); - tomcat.getHost().setDeployOnStartup(true); - Logging.configure(tomcat, props); - Connectors.configure(tomcat, props); - StandardContext webappContext = Webapp.configure(tomcat, props); - ProcessUtils.addSelfShutdownHook(this); + tomcat = new Tomcat(); + // Initialize directories + String basedir = tomcatBasedir().getAbsolutePath(); + tomcat.setBaseDir(basedir); + tomcat.getHost().setAppBase(basedir); + tomcat.getHost().setAutoDeploy(false); + tomcat.getHost().setCreateDirs(false); + tomcat.getHost().setDeployOnStartup(true); + Logging.configure(tomcat, props); + Connectors.configure(tomcat, props); + webappContext = Webapp.configure(tomcat, props); + try { tomcat.start(); + } catch (LifecycleException e) { + Throwables.propagate(e); + } + } - if (webappContext.getState().isAvailable()) { - ready = true; - tomcat.getServer().await(); - } - } catch (Exception e) { - throw new IllegalStateException("Fail to start web server", e); - } finally { - // Failed to start or received a shutdown command (should never occur as shutdown port is disabled) - terminate(); + boolean isReady() { + switch (webappContext.getState()) { + case NEW: + case INITIALIZING: + case INITIALIZED: + case STARTING_PREP: + case STARTING: + return false; + case STARTED: + return true; + default: + // problem, stopped or failed + throw new IllegalStateException("Webapp did not start"); } } @@ -82,25 +84,19 @@ class EmbeddedTomcat implements Terminable { return new File(props.value("sonar.path.temp"), "tc"); } - boolean isReady() { - return ready && tomcat != null; - } - - @Override - public void terminate() { - if (tomcat != null) { - synchronized (tomcat) { - if (tomcat.getServer().getState().isAvailable()) { - try { - tomcat.stop(); - tomcat.destroy(); - } catch (Exception e) { - LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to stop web service", e); - } - } + void terminate() { + if (tomcat.getServer().getState().isAvailable()) { + try { + tomcat.stop(); + tomcat.destroy(); + } catch (Exception e) { + LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to stop web server", e); } } - ready = false; FileUtils.deleteQuietly(tomcatBasedir()); } + + void awaitTermination() { + tomcat.getServer().await(); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java b/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java index d5bc4fde7de..87ffa5c5bee 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java @@ -19,18 +19,16 @@ */ package org.sonar.server.app; -import org.slf4j.LoggerFactory; -import org.sonar.process.ConfigurationUtils; import org.sonar.process.MinimumViableSystem; import org.sonar.process.MonitoredProcess; +import org.sonar.process.ProcessEntryPoint; import org.sonar.process.Props; -public class WebServer extends MonitoredProcess { +public class WebServer implements MonitoredProcess { private final EmbeddedTomcat tomcat; WebServer(Props props) throws Exception { - super(props); new MinimumViableSystem() .setRequiredJavaOption("file.encoding", "UTF-8") .check(); @@ -38,32 +36,32 @@ public class WebServer extends MonitoredProcess { } @Override - protected void doStart() { - try { - tomcat.start(); - } catch (Exception e) { - LoggerFactory.getLogger(getClass()).error("TC error", e); - } finally { - terminate(); - } + public void start() { + tomcat.start(); } @Override - protected void doTerminate() { + public boolean isReady() { + return tomcat.isReady(); + } + + @Override + public void terminate() { tomcat.terminate(); } @Override - protected boolean doIsReady() { - return tomcat.isReady(); + public void awaitTermination() { + tomcat.awaitTermination(); } /** * Can't be started as is. Needs to be bootstrapped by sonar-application */ public static void main(String[] args) throws Exception { - Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); - Logging.init(props); - new WebServer(props).start(); + ProcessEntryPoint entryPoint = ProcessEntryPoint.createForArguments(args); + Logging.init(entryPoint.getProps()); + WebServer server = new WebServer(entryPoint.getProps()); + entryPoint.launch(server); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java b/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java index f959faed106..8445efb22be 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/Webapp.java @@ -64,7 +64,6 @@ class Webapp { String key = entry.getKey().toString(); context.addParameter(key, entry.getValue().toString()); } - return context; } catch (Exception e) { diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java b/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java index 46bc0426333..33efbcd7e06 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/PlatformServletContextListener.java @@ -19,7 +19,7 @@ */ package org.sonar.server.platform; -import org.slf4j.LoggerFactory; +import com.google.common.base.Throwables; import javax.servlet.ServletContext; import javax.servlet.ServletContextEvent; @@ -46,9 +46,9 @@ public final class PlatformServletContextListener implements ServletContextListe // - server does not stop if webapp fails at startup // - the second listener for jruby on rails is started even if this listener fails. It generates // unexpected errors - LoggerFactory.getLogger(getClass()).error("Fail to start server", t); + // LoggerFactory.getLogger(getClass()).error("Fail to start server", t); stopQuietly(); - throw new IllegalStateException("Fail to start webapp", t); + throw Throwables.propagate(t); } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java b/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java index 3884439e166..5abc8a0eaef 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java +++ b/server/sonar-server/src/main/java/org/sonar/server/plugins/ServerPluginJarsInstaller.java @@ -211,9 +211,11 @@ public class ServerPluginJarsInstaller { private void deploy(DefaultPluginMetadata plugin) { LOG.info("Deploy plugin {}", Joiner.on(" / ").skipNulls().join(plugin.getName(), plugin.getVersion(), plugin.getImplementationBuild())); - Preconditions.checkState(plugin.isCompatibleWith(server.getVersion()), - "Plugin %s needs a more recent version of SonarQube than %s. At least %s is expected", - plugin.getKey(), server.getVersion(), plugin.getSonarVersion()); + if (!plugin.isCompatibleWith(server.getVersion())) { + throw MessageException.of(String.format( + "Plugin %s needs a more recent version of SonarQube than %s. At least %s is expected", + plugin.getKey(), server.getVersion(), plugin.getSonarVersion())); + } try { File pluginDeployDir = new File(fs.getDeployedPluginsDir(), plugin.getKey()); diff --git a/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java b/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java index be40c087f88..90cce6fb4ed 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/plugins/ServerPluginJarsInstallerTest.java @@ -85,7 +85,7 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); assertThat(new File(pluginsDir, "foo-plugin-1.0.jar")).exists().isFile(); PluginMetadata plugin = jarsInstaller.getMetadata("foo"); assertThat(plugin.getName()).isEqualTo("Foo"); @@ -101,7 +101,7 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).isEmpty(); } @Test @@ -115,7 +115,7 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); // do not copy foo 1.0 - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(2); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(2); assertThat(new File(pluginsDir, "foo-plugin-2.0.jar")).exists().isFile(); assertThat(new File(pluginsDir, "bar-plugin-1.0.jar")).exists().isFile(); PluginMetadata plugin = jarsInstaller.getMetadata("foo"); @@ -138,7 +138,7 @@ public class ServerPluginJarsInstallerTest { assertThat(plugin.isUseChildFirstClassLoader()).isFalse(); // check that the file is still present in extensions/plugins - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); assertThat(new File(pluginsDir, "foo-plugin-1.0.jar")).exists().isFile(); } @@ -151,13 +151,13 @@ public class ServerPluginJarsInstallerTest { // nothing to install but keep the file assertThat(jarsInstaller.getMetadata()).isEmpty(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); assertThat(new File(pluginsDir, "not-a-plugin.jar")).exists().isFile(); } @Test public void fail_if_plugin_requires_greater_SQ_version() throws Exception { - exception.expect(IllegalStateException.class); + exception.expect(MessageException.class); exception.expectMessage("Plugin switchoffviolations needs a more recent version of SonarQube than 2.0. At least 2.5 is expected"); when(upgradeStatus.isFreshInstall()).thenReturn(false); @@ -174,8 +174,8 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); - assertThat(FileUtils.listFiles(downloadsDir, new String[]{"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(downloadsDir, new String[] {"jar"}, false)).isEmpty(); assertThat(new File(pluginsDir, "foo-plugin-1.0.jar")).exists().isFile(); } @@ -187,8 +187,8 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); - assertThat(FileUtils.listFiles(downloadsDir, new String[]{"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(downloadsDir, new String[] {"jar"}, false)).isEmpty(); assertThat(new File(pluginsDir, "foo-plugin-2.0.jar")).exists().isFile(); } @@ -205,8 +205,8 @@ public class ServerPluginJarsInstallerTest { PluginMetadata plugin = jarsInstaller.getMetadata("foo"); assertThat(plugin).isNotNull(); assertThat(plugin.getVersion()).isEqualTo("2.0"); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); - assertThat(FileUtils.listFiles(downloadsDir, new String[]{"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(downloadsDir, new String[] {"jar"}, false)).isEmpty(); File installed = new File(pluginsDir, "foo-plugin-1.0.jar"); assertThat(installed).exists().isFile(); } @@ -218,7 +218,7 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).isEmpty(); assertThat(trashDir).doesNotExist(); } @@ -247,8 +247,8 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); jarsInstaller.uninstall("foo"); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).isEmpty(); - assertThat(FileUtils.listFiles(trashDir, new String[]{"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).isEmpty(); + assertThat(FileUtils.listFiles(trashDir, new String[] {"jar"}, false)).hasSize(1); assertThat(jarsInstaller.getUninstalls()).containsOnly("foo-plugin-1.0.jar"); } @@ -261,8 +261,8 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.uninstall("foo"); jarsInstaller.cancelUninstalls(); - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(1); - assertThat(FileUtils.listFiles(trashDir, new String[]{"jar"}, false)).hasSize(0); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(trashDir, new String[] {"jar"}, false)).hasSize(0); assertThat(jarsInstaller.getUninstalls()).isEmpty(); } @@ -274,10 +274,10 @@ public class ServerPluginJarsInstallerTest { jarsInstaller.install(); // do not deploy in extensions/plugins - assertThat(FileUtils.listFiles(pluginsDir, new String[]{"jar"}, false)).hasSize(0); + assertThat(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, false)).hasSize(0); // do not remove from lib/core-plugins - assertThat(FileUtils.listFiles(coreDir, new String[]{"jar"}, false)).hasSize(1); + assertThat(FileUtils.listFiles(coreDir, new String[] {"jar"}, false)).hasSize(1); PluginMetadata plugin = jarsInstaller.getMetadata("foo"); assertThat(plugin).isNotNull(); diff --git a/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java b/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java index e92fd5b936f..8270a46f06e 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/search/BaseIndexTest.java @@ -61,10 +61,9 @@ public class BaseIndexTest { Properties properties = new Properties(); properties.setProperty(IndexProperties.CLUSTER_NAME, clusterName); properties.setProperty(IndexProperties.NODE_PORT, clusterPort.toString()); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); properties.setProperty(SearchServer.SONAR_PATH_HOME, temp.getRoot().getAbsolutePath()); try { - searchServer = new SearchServer(new Props(properties), false, false); + searchServer = new SearchServer(new Props(properties)); } catch (Exception e) { e.printStackTrace(); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java b/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java index ce6fad22f77..1a935cb2394 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java +++ b/server/sonar-server/src/test/java/org/sonar/server/tester/ServerTester.java @@ -26,7 +26,6 @@ import org.apache.commons.lang.StringUtils; import org.junit.rules.ExternalResource; import org.sonar.api.database.DatabaseProperties; import org.sonar.api.resources.Language; -import org.sonar.process.MonitoredProcess; import org.sonar.process.NetworkUtils; import org.sonar.process.Props; import org.sonar.search.SearchServer; @@ -35,6 +34,7 @@ import org.sonar.server.search.IndexProperties; import org.sonar.server.ws.WsTester; import javax.annotation.Nullable; + import java.io.File; import java.util.Arrays; import java.util.List; @@ -71,13 +71,8 @@ public class ServerTester extends ExternalResource { Properties properties = new Properties(); properties.setProperty(IndexProperties.CLUSTER_NAME, clusterName); properties.setProperty(IndexProperties.NODE_PORT, clusterPort.toString()); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); properties.setProperty(SearchServer.SONAR_PATH_HOME, homeDir.getAbsolutePath()); - try { - searchServer = new SearchServer(new Props(properties), false, false); - } catch (Exception e) { - e.printStackTrace(); - } + searchServer = new SearchServer(new Props(properties)); } /** @@ -99,7 +94,6 @@ public class ServerTester extends ExternalResource { properties.setProperty(IndexProperties.CLUSTER_NAME, clusterName); properties.setProperty(IndexProperties.NODE_PORT, clusterPort.toString()); - properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); properties.setProperty("sonar.path.home", homeDir.getAbsolutePath()); properties.setProperty(DatabaseProperties.PROP_URL, "jdbc:h2:" + homeDir.getAbsolutePath() + "/h2"); diff --git a/server/sonar-web/pom.xml b/server/sonar-web/pom.xml index 0fcf1a9f973..324df14854a 100644 --- a/server/sonar-web/pom.xml +++ b/server/sonar-web/pom.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.codehaus.sonar</groupId> diff --git a/sonar-application/pom.xml b/sonar-application/pom.xml index 8e7f1f90012..94495baba59 100644 --- a/sonar-application/pom.xml +++ b/sonar-application/pom.xml @@ -1,6 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> @@ -25,6 +24,11 @@ <artifactId>sonar-process</artifactId> <version>${project.version}</version> </dependency> + <dependency> + <groupId>org.codehaus.sonar</groupId> + <artifactId>sonar-process-monitor</artifactId> + <version>${project.version}</version> + </dependency> <dependency> <groupId>com.google.code.findbugs</groupId> @@ -237,8 +241,8 @@ <phase>package</phase> <configuration> <target> - <checksum file="${project.build.directory}/sonarqube-${project.version}.zip" algorithm="md5"/> - <checksum file="${project.build.directory}/sonarqube-${project.version}.zip" algorithm="sha"/> + <checksum file="${project.build.directory}/sonarqube-${project.version}.zip" algorithm="md5" /> + <checksum file="${project.build.directory}/sonarqube-${project.version}.zip" algorithm="sha" /> </target> </configuration> <goals> diff --git a/sonar-application/src/main/java/org/sonar/application/App.java b/sonar-application/src/main/java/org/sonar/application/App.java index 76ac6a16605..600828ec1db 100644 --- a/sonar-application/src/main/java/org/sonar/application/App.java +++ b/sonar-application/src/main/java/org/sonar/application/App.java @@ -21,19 +21,18 @@ package org.sonar.application; import org.apache.commons.io.FilenameUtils; import org.apache.commons.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.sonar.process.JmxUtils; import org.sonar.process.MinimumViableSystem; -import org.sonar.process.Monitor; import org.sonar.process.ProcessLogging; import org.sonar.process.ProcessMXBean; -import org.sonar.process.ProcessUtils; -import org.sonar.process.ProcessWrapper; import org.sonar.process.Props; -import org.sonar.search.SearchServer; +import org.sonar.process.State; +import org.sonar.process.monitor.JavaCommand; +import org.sonar.process.monitor.Monitor; import java.io.File; +import java.util.ArrayList; +import java.util.List; import java.util.Properties; /** @@ -41,135 +40,88 @@ import java.util.Properties; */ public class App implements ProcessMXBean { - private Monitor monitor = new Monitor(); - private ProcessWrapper elasticsearch; - private ProcessWrapper server; - private boolean success = false; + private final Monitor monitor; public App() { - JmxUtils.registerMBean(this, "SonarQube"); - ProcessUtils.addSelfShutdownHook(this); + this(Monitor.create()); } - public void start(Props props) throws InterruptedException { - try { - Logger logger = LoggerFactory.getLogger(getClass()); + App(Monitor monitor) { + this.monitor = monitor; + JmxUtils.registerMBean(this, "SonarQube"); + } - monitor.start(); + public void start(Props props) { + monitor.start(createCommands(props)); + monitor.awaitTermination(); + } - File homeDir = props.nonNullValueAsFile("sonar.path.home"); - File tempDir = props.nonNullValueAsFile("sonar.path.temp"); - elasticsearch = new ProcessWrapper(JmxUtils.SEARCH_SERVER_NAME); - elasticsearch + List<JavaCommand> createCommands(Props props) { + List<JavaCommand> commands = new ArrayList<JavaCommand>(); + File homeDir = props.nonNullValueAsFile("sonar.path.home"); + File tempDir = props.nonNullValueAsFile("sonar.path.temp"); + JavaCommand elasticsearch = new JavaCommand(JmxUtils.SEARCH_SERVER_NAME); + elasticsearch + .setWorkDir(homeDir) + .setJmxPort(props.valueAsInt(DefaultSettings.SEARCH_JMX_PORT)) + .addJavaOptions(props.value(DefaultSettings.SEARCH_JAVA_OPTS)) + .setTempDir(tempDir.getAbsoluteFile()) + .setClassName("org.sonar.search.SearchServer") + .setArguments(props.rawProperties()) + .addClasspath("./lib/common/*") + .addClasspath("./lib/search/*"); + commands.add(elasticsearch); + + // do not yet start SQ in cluster mode. See SONAR-5483 & SONAR-5391 + if (StringUtils.isEmpty(props.value(DefaultSettings.CLUSTER_MASTER))) { + JavaCommand webServer = new JavaCommand(JmxUtils.WEB_SERVER_NAME) .setWorkDir(homeDir) - .setJmxPort(props.valueAsInt(DefaultSettings.SEARCH_JMX_PORT)) - .addJavaOpts(props.value(DefaultSettings.SEARCH_JAVA_OPTS)) - .setTempDirectory(tempDir.getAbsoluteFile()) - .setClassName("org.sonar.search.SearchServer") - .addProperties(props.rawProperties()) + .setJmxPort(props.valueAsInt(DefaultSettings.WEB_JMX_PORT)) + .addJavaOptions(props.nonNullValue(DefaultSettings.WEB_JAVA_OPTS)) + .setTempDir(tempDir.getAbsoluteFile()) + // required for logback tomcat valve + .setEnvVariable("sonar.path.logs", props.nonNullValue("sonar.path.logs")) + .setClassName("org.sonar.server.app.WebServer") + .setArguments(props.rawProperties()) .addClasspath("./lib/common/*") - .addClasspath("./lib/search/*"); - if (elasticsearch.execute()) { - monitor.monitor(elasticsearch); - if (elasticsearch.waitForReady()) { - logger.info("search server is up"); - - // do not yet start SQ in cluster mode. See SONAR-5483 & SONAR-5391 - if (StringUtils.isEmpty(props.value(DefaultSettings.CLUSTER_MASTER))) { - server = new ProcessWrapper(JmxUtils.WEB_SERVER_NAME) - .setWorkDir(homeDir) - .setJmxPort(props.valueAsInt(DefaultSettings.WEB_JMX_PORT)) - .addJavaOpts(props.nonNullValue(DefaultSettings.WEB_JAVA_OPTS)) - .setTempDirectory(tempDir.getAbsoluteFile()) - // required for logback tomcat valve - .setLogDir(props.nonNullValueAsFile("sonar.path.logs")) - .setClassName("org.sonar.server.app.WebServer") - .addProperties(props.rawProperties()) - .addClasspath("./lib/common/*") - .addClasspath("./lib/server/*"); - String driverPath = props.value(JdbcSettings.PROPERTY_DRIVER_PATH); - if (driverPath != null) { - server.addClasspath(driverPath); - } - if (server.execute()) { - monitor.monitor(server); - if (server.waitForReady()) { - success = true; - logger.info("web server is up"); - } - } - } else { - success = true; - } - } + .addClasspath("./lib/server/*"); + String driverPath = props.value(JdbcSettings.PROPERTY_DRIVER_PATH); + if (driverPath != null) { + webServer.addClasspath(driverPath); } - } finally { - monitor.join(); - terminate(); + commands.add(webServer); } + return commands; } - static String starPath(File homeDir, String relativePath) { - File dir = new File(homeDir, relativePath); - return FilenameUtils.concat(dir.getAbsolutePath(), "*"); + @Override + public void terminate() { + monitor.stop(); } @Override public boolean isReady() { - return monitor.isAlive(); + return monitor.getState() == State.STARTED; } @Override - public long ping() { - return System.currentTimeMillis(); - } + public void ping() { - @Override - public void terminate() { - if (monitor != null && monitor.isAlive()) { - monitor.terminate(); - monitor = null; - } - if (server != null) { - server.terminate(); - server = null; - } - if (elasticsearch != null) { - elasticsearch.terminate(); - elasticsearch = null; - } } - private boolean isSuccess() { - return success; + static String starPath(File homeDir, String relativePath) { + File dir = new File(homeDir, relativePath); + return FilenameUtils.concat(dir.getAbsolutePath(), "*"); } - public static void main(String[] args) { + public static void main(String[] args) throws Exception { new MinimumViableSystem().check(); CommandLineParser cli = new CommandLineParser(); Properties rawProperties = cli.parseArguments(args); - Props props; - - try { - props = new PropsBuilder(rawProperties, new JdbcSettings()).build(); - new ProcessLogging().configure(props, "/org/sonar/application/logback.xml"); - } catch (Exception e) { - throw new IllegalStateException(e); - } + Props props = new PropsBuilder(rawProperties, new JdbcSettings()).build(); + new ProcessLogging().configure(props, "/org/sonar/application/logback.xml"); App app = new App(); - ProcessUtils.addSelfShutdownHook(app); - try { - // start and wait for shutdown command - if (props.contains(SearchServer.ES_CLUSTER_INET)) { - LoggerFactory.getLogger(App.class).info("SonarQube slave configured to join SonarQube master : {}", props.value(SearchServer.ES_CLUSTER_INET)); - } - app.start(props); - } catch (InterruptedException e) { - LoggerFactory.getLogger(App.class).info("interrupted"); - } finally { - LoggerFactory.getLogger(App.class).info("stopped"); - System.exit(app.isSuccess() ? 0 : 1); - } + app.start(props); } } |