diff options
17 files changed, 595 insertions, 674 deletions
@@ -1,6 +1,6 @@ #!/bin/sh -mvn clean install -DskipTests -Denforcer.skip=true -pl server/sonar-search,server/sonar-process,sonar-application +mvn clean install -DskipTests -Denforcer.skip=true -pl :sonar-search,:sonar-process -amd if [[ "$OSTYPE" == "darwin"* ]]; then OS='macosx-universal-64' diff --git a/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java b/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java new file mode 100644 index 00000000000..b1c28932118 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/JmxUtils.java @@ -0,0 +1,51 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import javax.management.MBeanServer; +import javax.management.MalformedObjectNameException; +import javax.management.ObjectName; + +import java.lang.management.ManagementFactory; + +public class JmxUtils { + private JmxUtils() { + // only static stuff + } + + public static ObjectName objectName(String name) { + try { + return new ObjectName("org.sonar", "name", name); + } catch (MalformedObjectNameException e) { + throw new IllegalStateException("Cannot create ObjectName for " + name, e); + } + } + + public static void registerMBean(Object mbean, String name) { + try { + MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); + mbeanServer.registerMBean(mbean, objectName(name)); + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new IllegalStateException("Fail to register JMX MBean named " + name, e); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/Monitor.java b/server/sonar-process/src/main/java/org/sonar/process/Monitor.java index 7f11bf3a378..0900c110973 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/Monitor.java +++ b/server/sonar-process/src/main/java/org/sonar/process/Monitor.java @@ -31,89 +31,98 @@ import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -public class Monitor extends Thread { - - private static final long MAX_TIME = 15000L; +public class Monitor extends Thread implements Terminatable { + private static final long PING_DELAY_MS = 3000L; + private static final long TIMEOUT_MS = 15000L; private final static Logger LOGGER = LoggerFactory.getLogger(Monitor.class); private volatile List<ProcessWrapper> processes; private volatile Map<String, Long> pings; + private final ScheduledFuture<?> watch; + private final ScheduledExecutorService monitor; - private ProcessWatch processWatch; - private ScheduledFuture<?> watch; - private ScheduledExecutorService monitor; - + /** + * Starts another thread to send ping to all registered processes + */ public Monitor() { + super("Process Monitor"); processes = new ArrayList<ProcessWrapper>(); pings = new HashMap<String, Long>(); monitor = Executors.newScheduledThreadPool(1); - processWatch = new ProcessWatch(); - watch = monitor.scheduleWithFixedDelay(processWatch, 0, 3, TimeUnit.SECONDS); + watch = monitor.scheduleWithFixedDelay(new ProcessWatch(), 0L, PING_DELAY_MS, TimeUnit.MILLISECONDS); } - public void registerProcess(ProcessWrapper processWrapper) { - processes.add(processWrapper); - pings.put(processWrapper.getName(), System.currentTimeMillis()); - for (int i = 0; i < 10; i++) { - if (processWrapper.getProcessMXBean() == null || !processWrapper.getProcessMXBean().isReady()) { - try { - Thread.sleep(500L); - } catch (InterruptedException e) { - throw new IllegalStateException("Could not register process in Monitor", e); - } - } + private class ProcessWatch extends Thread { + private ProcessWatch() { + super("Process Ping"); } - processWrapper.start(); - } - private class ProcessWatch implements Runnable { + @Override public void run() { for (ProcessWrapper process : processes) { try { - if (process.getProcessMXBean() != null) { - long time = process.getProcessMXBean().ping(); - LOGGER.debug("PINGED '{}'", process.getName()); + ProcessMXBean mBean = process.getProcessMXBean(); + if (mBean != null) { + long time = mBean.ping(); pings.put(process.getName(), time); } } catch (Exception e) { - LOGGER.error("Error while pinging {}", process.getName(), e); - terminate(); + // fail to ping, do nothing } } } } - private boolean processIsValid(ProcessWrapper process) { - long now = System.currentTimeMillis(); - return (now - pings.get(process.getName())) < MAX_TIME; + /** + * Registers and monitors process. Note that process is probably not ready yet. + */ + public void registerProcess(ProcessWrapper process) throws InterruptedException { + processes.add(process); + pings.put(process.getName(), System.currentTimeMillis()); + // starts a monitoring thread + process.start(); } + private boolean processIsValid(ProcessWrapper processWrapper) { + if (ProcessUtils.isAlive(processWrapper.process())) { + long now = System.currentTimeMillis(); + return now - pings.get(processWrapper.getName()) < TIMEOUT_MS; + } + return false; + } + + /** + * 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 { while (true) { for (ProcessWrapper process : processes) { if (!processIsValid(process)) { LOGGER.warn("Monitor::run() -- Process '{}' is not valid. Exiting monitor", process.getName()); - this.interrupt(); + interrupt(); } } - Thread.sleep(3000L); + Thread.sleep(PING_DELAY_MS); } } catch (InterruptedException e) { - LOGGER.debug("Monitoring thread is interrupted."); + LOGGER.debug("Monitoring thread is interrupted"); } finally { terminate(); } } + @Override public void terminate() { - if (monitor != null) { + processes.clear(); + if (!monitor.isShutdown()) { monitor.shutdownNow(); + } + if (!watch.isCancelled()) { watch.cancel(true); - watch = null; - processWatch = null; } } - } 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..d75a2fc4588 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/MonitoredProcess.java @@ -0,0 +1,126 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.apache.commons.lang.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +public abstract class MonitoredProcess implements ProcessMXBean { + + public static final String NAME_PROPERTY = "pName"; + private static final long AUTOKILL_TIMEOUT_MS = 15000L; + private static final long AUTOKILL_CHECK_DELAY_MS = 5000L; + public static final String MISSING_NAME_ARGUMENT = "Missing Name argument"; + + private Long lastPing; + private final String name; + protected final Props props; + private ScheduledFuture<?> pingTask = null; + private ScheduledExecutorService monitor; + + protected MonitoredProcess(Props props) throws Exception { + this.props = props; + this.name = props.of(NAME_PROPERTY); + + // Testing required properties + if (StringUtils.isEmpty(name)) { + throw new IllegalStateException(MISSING_NAME_ARGUMENT); + } + + JmxUtils.registerMBean(this, name); + ProcessUtils.addSelfShutdownHook(this); + } + + public final void start() { + if (monitor != null) { + throw new IllegalStateException("Already started"); + } + + Logger logger = LoggerFactory.getLogger(getClass()); + logger.debug("Process[{}] starting", name); + scheduleAutokill(); + doStart(); + logger.debug("Process[{}] started", name); + } + + /** + * If the process does not receive pings during the max allowed period, then + * process auto-kills + */ + private void scheduleAutokill() { + final Runnable breakOnMissingPing = new Runnable() { + @Override + public void run() { + long time = System.currentTimeMillis(); + if (time - lastPing > AUTOKILL_TIMEOUT_MS) { + terminate(); + } + } + }; + lastPing = System.currentTimeMillis(); + monitor = Executors.newScheduledThreadPool(1); + pingTask = monitor.scheduleWithFixedDelay(breakOnMissingPing, AUTOKILL_CHECK_DELAY_MS, AUTOKILL_CHECK_DELAY_MS, TimeUnit.MILLISECONDS); + } + + @Override + public final long ping() { + this.lastPing = System.currentTimeMillis(); + return lastPing; + } + + @Override + public final void terminate() { + if (monitor != null) { + monitor.shutdownNow(); + monitor = null; + if (pingTask != null) { + pingTask.cancel(true); + pingTask = null; + } + try { + doTerminate(); + } catch (Exception e) { + LoggerFactory.getLogger(getClass()).error("Fail to terminate " + name, e); + // do not propagate exception + } + } + } + + @Override + public final boolean isReady() { + try { + return doIsReady(); + } catch (Exception ignored) { + return false; + } + } + + protected abstract void doStart(); + + protected abstract void doTerminate(); + + protected abstract boolean doIsReady(); +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/Process.java b/server/sonar-process/src/main/java/org/sonar/process/Process.java deleted file mode 100644 index 123f7061ee9..00000000000 --- a/server/sonar-process/src/main/java/org/sonar/process/Process.java +++ /dev/null @@ -1,143 +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.lang.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import javax.management.InstanceAlreadyExistsException; -import javax.management.MBeanRegistrationException; -import javax.management.MBeanServer; -import javax.management.MalformedObjectNameException; -import javax.management.NotCompliantMBeanException; -import javax.management.ObjectName; -import java.lang.management.ManagementFactory; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -public abstract class Process implements ProcessMXBean { - - public static final String NAME_PROPERTY = "pName"; - public static final String PORT_PROPERTY = "pPort"; - public static final String MISSING_NAME_ARGUMENT = "Missing Name argument"; - - private Long lastPing; - private String name; - private Integer port; - - protected final Props props; - - private static final long MAX_ALLOWED_TIME = 15000L; - private ScheduledFuture<?> pingTask = null; - private ScheduledExecutorService monitor = Executors.newScheduledThreadPool(1); - final Runnable breakOnMissingPing = new Runnable() { - public void run() { - long time = System.currentTimeMillis(); - if (time - lastPing > MAX_ALLOWED_TIME) { - LoggerFactory.getLogger(getClass()).warn("Did not get a check-in for {}ms. Initiate shutdown", time - lastPing); - terminate(); - } - } - }; - - protected Process(Props props) { - this.props = props; - init(); - } - - private void init() { - this.name = props.of(NAME_PROPERTY, null); - this.port = props.intOf(PORT_PROPERTY); - - // Testing required properties - if (StringUtils.isEmpty(this.name)) { - throw new IllegalStateException(MISSING_NAME_ARGUMENT); - } - - MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); - try { - mbeanServer.registerMBean(this, this.getObjectName()); - } catch (InstanceAlreadyExistsException e) { - throw new IllegalStateException("Process already exists in current JVM", e); - } catch (MBeanRegistrationException e) { - throw new IllegalStateException("Could not register process as MBean", e); - } catch (NotCompliantMBeanException e) { - throw new IllegalStateException("Process is not a compliant MBean", e); - } - - Thread shutdownHook = new Thread(new Runnable() { - @Override - public void run() { - terminate(); - } - }); - Runtime.getRuntime().addShutdownHook(shutdownHook); - } - - public ObjectName getObjectName() { - return objectNameFor(name); - } - - static public ObjectName objectNameFor(String name) { - try { - return new ObjectName("org.sonar", "name", name); - } catch (MalformedObjectNameException e) { - throw new IllegalStateException("Cannot create ObjectName for " + name, e); - } - } - - public long ping() { - this.lastPing = System.currentTimeMillis(); - return lastPing; - } - - public abstract void onStart(); - - public abstract void onTerminate(); - - public final void start() { - Logger logger = LoggerFactory.getLogger(getClass()); - logger.debug("Process[{}] starting", name); - if (this.port != null) { - lastPing = System.currentTimeMillis(); - pingTask = monitor.scheduleWithFixedDelay(breakOnMissingPing, 5, 5, TimeUnit.SECONDS); - } - this.onStart(); - logger.debug("Process[{}] started", name); - } - - public final void terminate() { - Logger logger = LoggerFactory.getLogger(getClass()); - logger.debug("Process[{}] terminating", name); - if (monitor != null) { - this.monitor.shutdownNow(); - this.monitor = null; - if (this.pingTask != null) { - this.pingTask.cancel(true); - this.pingTask = null; - } - this.onTerminate(); - } - logger.debug("Process[{}] terminated", name); - } -} diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java index 85ff3400ffd..ed306eeba82 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessMXBean.java @@ -19,15 +19,9 @@ */ package org.sonar.process; -public interface ProcessMXBean { - - String IS_READY = "isReady"; - String PING = "ping"; - String TERMINATE = "terminate"; +public interface ProcessMXBean extends Terminatable { boolean isReady(); long ping(); - - void terminate(); } diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java new file mode 100644 index 00000000000..307743ba073 --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessUtils.java @@ -0,0 +1,70 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +import org.apache.commons.io.IOUtils; + +import javax.annotation.Nullable; + +public class ProcessUtils { + private ProcessUtils() { + // only static stuff + } + + public static boolean isAlive(@Nullable Process process) { + if (process == null) { + return false; + } + try { + process.exitValue(); + return false; + } catch (IllegalThreadStateException e) { + return true; + } + } + + public static void destroyQuietly(@Nullable Process process) { + if (process != null && isAlive(process)) { + try { + process.destroy(); + } catch (Exception ignored) { + // ignored + } + } + } + + public static void addSelfShutdownHook(final Terminatable terminatable) { + Thread shutdownHook = new Thread(new Runnable() { + @Override + public void run() { + terminatable.terminate(); + } + }); + Runtime.getRuntime().addShutdownHook(shutdownHook); + } + + public static void closeStreams(@Nullable Process process) { + if (process != null) { + IOUtils.closeQuietly(process.getInputStream()); + IOUtils.closeQuietly(process.getOutputStream()); + IOUtils.closeQuietly(process.getErrorStream()); + } + } +} diff --git a/server/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java b/server/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java index 7cecc3001de..7231df67ed1 100644 --- a/server/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java +++ b/server/sonar-process/src/main/java/org/sonar/process/ProcessWrapper.java @@ -25,12 +25,14 @@ 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.remote.JMXConnector; import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; + import java.io.BufferedReader; import java.io.File; import java.io.FileOutputStream; @@ -48,13 +50,18 @@ 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 { +public class ProcessWrapper extends Thread implements Terminatable { private final static Logger LOGGER = LoggerFactory.getLogger(ProcessWrapper.class); + public static final long READY_TIMEOUT_MS = 120000L; private String processName, className; private int jmxPort = -1; @@ -64,7 +71,7 @@ public class ProcessWrapper extends Thread { private final Properties properties = new Properties(); private File workDir; private File propertiesFile; - private java.lang.Process process; + private Process process; private StreamGobbler errorGobbler; private StreamGobbler outputGobbler; private ProcessMXBean processMXBean; @@ -115,7 +122,16 @@ public class ProcessWrapper extends Thread { return this; } - public ProcessWrapper execute() { + @CheckForNull + 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>(); command.add(buildJavaCommand()); command.addAll(javaOpts); @@ -137,7 +153,11 @@ public class ProcessWrapper extends Thread { outputGobbler.start(); errorGobbler.start(); processMXBean = waitForJMX(); - return this; + if (processMXBean == null) { + terminate(); + return false; + } + return true; } catch (IOException e) { throw new IllegalStateException("Fail to start command: " + StringUtils.join(command, " "), e); } @@ -146,13 +166,15 @@ public class ProcessWrapper extends Thread { @Override public void run() { try { - process.waitFor(); - } catch (InterruptedException e) { + if (ProcessUtils.isAlive(process)) { + process.waitFor(); + } + } catch (Exception e) { LOGGER.info("ProcessThread has been interrupted. Killing process."); } finally { waitUntilFinish(outputGobbler); waitUntilFinish(errorGobbler); - closeStreams(process); + ProcessUtils.closeStreams(process); FileUtils.deleteQuietly(propertiesFile); processMXBean = null; } @@ -177,14 +199,6 @@ public class ProcessWrapper extends Thread { } } - private void closeStreams(@Nullable java.lang.Process process) { - if (process != null) { - IOUtils.closeQuietly(process.getInputStream()); - IOUtils.closeQuietly(process.getOutputStream()); - IOUtils.closeQuietly(process.getErrorStream()); - } - } - private String buildJavaCommand() { String separator = System.getProperty("file.separator"); return System.getProperty("java.home") @@ -211,8 +225,7 @@ public class ProcessWrapper extends Thread { propertiesFile = File.createTempFile("sq-conf", "properties"); Properties props = new Properties(); props.putAll(properties); - props.put(Process.NAME_PROPERTY, processName); - props.put(Process.PORT_PROPERTY, String.valueOf(jmxPort)); + props.put(MonitoredProcess.NAME_PROPERTY, processName); OutputStream out = new FileOutputStream(propertiesFile); props.store(out, "Temporary properties file for Process [" + getName() + "]"); out.close(); @@ -222,52 +235,84 @@ public class ProcessWrapper extends Thread { } } - private ProcessMXBean waitForJMX() { - Exception exception = null; + /** + * Wait for JMX RMI to be ready. Return <code>null</code> + */ + @CheckForNull + private ProcessMXBean waitForJMX() throws UnknownHostException, MalformedURLException { + String path = "/jndi/rmi://" + InetAddress.getLocalHost().getHostName() + ":" + jmxPort + "/jmxrmi"; + JMXServiceURL jmxUrl = new JMXServiceURL("rmi", InetAddress.getLocalHost().getHostAddress(), jmxPort, path); + for (int i = 0; i < 5; i++) { try { - Thread.sleep(1000); - } catch (InterruptedException e) { - throw new IllegalStateException("Could not connect to JMX server", e); - } - LOGGER.debug("Try #{} to connect to JMX server for process '{}'", i, processName); - try { - String protocol = "rmi"; - String path = "/jndi/rmi://" + InetAddress.getLocalHost().getHostName() + ":" + jmxPort + "/jmxrmi"; - JMXServiceURL jmxUrl = new JMXServiceURL(protocol, InetAddress.getLocalHost().getHostAddress(), jmxPort, path); + Thread.sleep(1000L); + LOGGER.debug("Try #{} to connect to JMX server for process '{}'", i, processName); JMXConnector jmxConnector = JMXConnectorFactory.connect(jmxUrl, null); MBeanServerConnection mBeanServer = jmxConnector.getMBeanServerConnection(); - ProcessMXBean bean = JMX.newMBeanProxy(mBeanServer, Process.objectNameFor(processName), ProcessMXBean.class); - LOGGER.info("{} process up and running, listening to its state with url: '{}'", getName(), jmxUrl.toString()); + ProcessMXBean bean = JMX.newMBeanProxy(mBeanServer, JmxUtils.objectName(processName), ProcessMXBean.class); return bean; - } catch (MalformedURLException e) { - throw new IllegalStateException("JMXUrl is not valid", e); - } catch (UnknownHostException e) { - throw new IllegalStateException("Could not get hostname", e); - } catch (IOException e) { - exception = e; + } catch (Exception ignored) { + // ignored } } - throw new IllegalStateException("Could not connect to JMX service", exception); + // failed to connect + return null; } + @Override public void terminate() { if (processMXBean != null) { - LOGGER.info("Stopping {} process", getName()); - processMXBean.terminate(); + // 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 { - this.join(); - } catch (InterruptedException e) { + Runnable killerTask = new Runnable() { + @Override + public void run() { + ProcessUtils.destroyQuietly(process); + } + }; + + ScheduledFuture killerFuture = killer.schedule(killerTask, 30, TimeUnit.SECONDS); + LOGGER.info("Stopping {} process", getName()); + processMXBean.terminate(); + killerFuture.cancel(true); + processMXBean = null; + LOGGER.info("{} process stopped", getName()); + + } catch (Exception e) { + LOGGER.warn("Failed to terminate " + getName(), e); + } finally { + killer.shutdownNow(); + } + } else { + // process is not monitored through JMX, but killing it though + ProcessUtils.destroyQuietly(process); + } + } + + public boolean waitForReady() throws InterruptedException { + if (processMXBean == null) { + return false; + } + long now = 0; + long wait = 500L; + while (now < READY_TIMEOUT_MS) { + try { + if (processMXBean.isReady()) { + return true; + } + } catch (Exception e) { // ignore } - processMXBean = null; - LOGGER.info("{} process stopped", getName()); + Thread.sleep(wait); + now += wait; } + return false; } private static class StreamGobbler extends Thread { private final InputStream is; - private volatile Exception exception; private final String pName; StreamGobbler(InputStream is, String name) { @@ -285,17 +330,13 @@ public class ProcessWrapper extends Thread { while ((line = br.readLine()) != null) { LOGGER.info(pName + " > " + line); } - } catch (IOException ioe) { - exception = ioe; + } catch (IOException ignored) { + // ignored } finally { IOUtils.closeQuietly(br); IOUtils.closeQuietly(isr); } } - - public Exception getException() { - return exception; - } } } diff --git a/server/sonar-process/src/main/java/org/sonar/process/Terminatable.java b/server/sonar-process/src/main/java/org/sonar/process/Terminatable.java new file mode 100644 index 00000000000..403ba5895de --- /dev/null +++ b/server/sonar-process/src/main/java/org/sonar/process/Terminatable.java @@ -0,0 +1,30 @@ +/* + * SonarQube, open source software quality management tool. + * Copyright (C) 2008-2014 SonarSource + * mailto:contact AT sonarsource DOT com + * + * SonarQube is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * SonarQube is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +package org.sonar.process; + +/** + * This interface was not named Stopable in order to not conflict with {@link Thread#stop()}. + */ +public interface Terminatable { + /** + * Stops pending work. Must <b>not</b> throw an exception on error. + */ + void terminate(); +} diff --git a/server/sonar-process/src/test/java/org/sonar/process/ProcessTest.java b/server/sonar-process/src/test/java/org/sonar/process/ProcessTest.java deleted file mode 100644 index 3c9ccd5a5ff..00000000000 --- a/server/sonar-process/src/test/java/org/sonar/process/ProcessTest.java +++ /dev/null @@ -1,179 +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.Ignore; -import org.junit.Test; - -import javax.management.JMX; -import javax.management.MBeanServer; -import java.io.IOException; -import java.lang.management.ManagementFactory; -import java.net.ServerSocket; -import java.util.Properties; - -import static junit.framework.TestCase.fail; -import static org.fest.assertions.Assertions.assertThat; - -public class ProcessTest { - - int freePort; - - @Before - public void setup() throws IOException { - ServerSocket socket = new ServerSocket(0); - freePort = socket.getLocalPort(); - socket.close(); - } - - Process process; - - @After - public void tearDown() throws Exception { - if (process != null) { - MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); - mbeanServer.unregisterMBean(process.getObjectName()); - } - } - - @Test - public void fails_invalid_name() { - try { - Process.objectNameFor("::"); - fail(); - } catch (Exception e) { - assertThat(e.getMessage()).isEqualTo("Cannot create ObjectName for ::"); - } - } - - @Test - public void fail_missing_properties() { - Properties properties = new Properties(); - try { - new TestProcess(new Props(properties)); - } catch (Exception e) { - assertThat(e.getMessage()).isEqualTo(Process.MISSING_NAME_ARGUMENT); - } - } - - @Test - public void should_register_mbean() throws Exception { - - MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); - - Properties properties = new Properties(); - properties.setProperty(Process.NAME_PROPERTY, "TEST"); - Props props = new Props(properties); - process = new TestProcess(props); - - // 0 Can have a valid ObjectName - assertThat(process.getObjectName()).isNotNull(); - - // 1 assert that process MBean is registered - assertThat(mbeanServer.isRegistered(process.getObjectName())).isTrue(); - - // 2 assert that we cannot make another Process in the same JVM - try { - process = new TestProcess(props); - fail(); - } catch (IllegalStateException e) { - assertThat(e.getMessage()).isEqualTo("Process already exists in current JVM"); - } - } - - @Test(timeout = 5000L) - @Ignore - public void should_stop_explicit() throws Exception { - Properties properties = new Properties(); - properties.setProperty(Process.NAME_PROPERTY, "TEST"); - Props props = new Props(properties); - process = new TestProcess(props); - - MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); - final ProcessMXBean processMXBean = JMX.newMBeanProxy(mbeanServer, process.getObjectName(), ProcessMXBean.class); - - Thread procThread = new Thread(new Runnable() { - @Override - public void run() { - process.start(); - } - }); - - // 0. Process is loaded but not ready yet. - assertThat(processMXBean.isReady()).isFalse(); - - // 1. Pretend the process has started - procThread.start(); - Thread.sleep(200); - assertThat(procThread.isAlive()).isTrue(); - assertThat(processMXBean.isReady()).isTrue(); - - // 2. Stop the process through Management - processMXBean.terminate(); - procThread.join(); - } - - @Test(timeout = 15000L) - @Ignore - public void should_stop_implicit() throws Exception { - Properties properties = new Properties(); - properties.setProperty(Process.NAME_PROPERTY, "TEST"); - properties.setProperty(Process.PORT_PROPERTY, Integer.toString(freePort)); - Props props = new Props(properties); - process = new TestProcess(props); - - process.start(); - } - - public static class TestProcess extends Process { - - private boolean ready = false; - private boolean running = false; - - public TestProcess(Props props) { - super(props); - running = true; - } - - @Override - public void onStart() { - ready = true; - while (running) { - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - } - - @Override - public void onTerminate() { - running = false; - } - - @Override - public boolean isReady() { - return ready; - } - } -} diff --git a/server/sonar-search/src/main/java/org/sonar/search/ElasticSearch.java b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java index bfdb5c909a8..6789aa650f7 100644 --- a/server/sonar-search/src/main/java/org/sonar/search/ElasticSearch.java +++ b/server/sonar-search/src/main/java/org/sonar/search/SearchServer.java @@ -26,13 +26,13 @@ import org.elasticsearch.node.Node; import org.elasticsearch.node.NodeBuilder; import org.slf4j.LoggerFactory; import org.sonar.process.ConfigurationUtils; -import org.sonar.process.Process; +import org.sonar.process.MonitoredProcess; import org.sonar.process.Props; import org.sonar.search.script.ListUpdate; import java.io.File; -public class ElasticSearch extends Process { +public class SearchServer extends MonitoredProcess { public static final String ES_DEBUG_PROPERTY = "esDebug"; public static final String ES_PORT_PROPERTY = "sonar.search.port"; @@ -40,84 +40,26 @@ public class ElasticSearch extends Process { private Node node; - ElasticSearch(Props props) { + SearchServer(Props props) throws Exception { super(props); } @Override - public boolean isReady() { - try { - return (node.client().admin().cluster().prepareHealth() - .setWaitForYellowStatus() - .setTimeout(TimeValue.timeValueSeconds(3L)) - .get() - .getStatus() != ClusterHealthStatus.RED); - } catch (Exception e) { - return false; - } - } - - private void initAnalysis(ImmutableSettings.Builder esSettings) { - esSettings - .put("index.mapper.dynamic", false) - - // 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 - .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 - .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 - .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 - .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 - .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 - .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) - .put("index.analysis.filter.word_filter.catenate_numbers", true) - .put("index.analysis.filter.word_filter.catenate_all", true) - .put("index.analysis.filter.word_filter.split_on_case_change", true) - .put("index.analysis.filter.word_filter.preserve_original", true) - .put("index.analysis.filter.word_filter.split_on_numerics", true) - .put("index.analysis.filter.word_filter.stem_english_possessive", true) - - // Path Analyzer - .put("index.analysis.analyzer.path_analyzer.type", "custom") - .put("index.analysis.analyzer.path_analyzer.tokenizer", "path_hierarchy"); - + protected boolean doIsReady() { + return (node.client().admin().cluster().prepareHealth() + .setWaitForYellowStatus() + .setTimeout(TimeValue.timeValueSeconds(3L)) + .get() + .getStatus() != ClusterHealthStatus.RED); } @Override - public void onStart() { + protected void doStart() { String dataDir = props.of("sonar.path.data"); Integer port = props.intOf(ES_PORT_PROPERTY); String clusterName = props.of(ES_CLUSTER_PROPERTY); - LoggerFactory.getLogger(ElasticSearch.class).info("Starting ES[{}] on port: {}", clusterName, port); + LoggerFactory.getLogger(SearchServer.class).info("Starting ES[{}] on port: {}", clusterName, port); ImmutableSettings.Builder esSettings = ImmutableSettings.settingsBuilder() .put("es.foreground", "yes") @@ -165,15 +107,70 @@ public class ElasticSearch extends Process { } } - public void onTerminate() { + private void initAnalysis(ImmutableSettings.Builder esSettings) { + esSettings + .put("index.mapper.dynamic", false) + + // 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 + .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 + .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 + .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 + .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 + .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 + .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) + .put("index.analysis.filter.word_filter.catenate_numbers", true) + .put("index.analysis.filter.word_filter.catenate_all", true) + .put("index.analysis.filter.word_filter.split_on_case_change", true) + .put("index.analysis.filter.word_filter.preserve_original", true) + .put("index.analysis.filter.word_filter.split_on_numerics", true) + .put("index.analysis.filter.word_filter.stem_english_possessive", true) + + // Path Analyzer + .put("index.analysis.analyzer.path_analyzer.type", "custom") + .put("index.analysis.analyzer.path_analyzer.tokenizer", "path_hierarchy"); + + } + + @Override + protected void doTerminate() { if (node != null && !node.isClosed()) { node.close(); node = null; } } - public static void main(String... args) throws InterruptedException { + public static void main(String... args) throws Exception { Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); - new ElasticSearch(props).start(); + new SearchServer(props).start(); } } diff --git a/server/sonar-search/src/main/resources/logback.xml b/server/sonar-search/src/main/resources/logback.xml index 1800be76f8f..648ece82e28 100644 --- a/server/sonar-search/src/main/resources/logback.xml +++ b/server/sonar-search/src/main/resources/logback.xml @@ -27,21 +27,9 @@ </encoder> </appender> - <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> - <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> - <level>WARN</level> - </filter> - <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="INFO"/> <appender-ref ref="LOGFILE"/> - <appender-ref ref="CONSOLE"/> </root> </configuration> diff --git a/server/sonar-search/src/test/java/org/sonar/search/ElasticSearchTest.java b/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java index 4816e77bbdd..a97c51a9c62 100644 --- a/server/sonar-search/src/test/java/org/sonar/search/ElasticSearchTest.java +++ b/server/sonar-search/src/test/java/org/sonar/search/SearchServerTest.java @@ -28,26 +28,27 @@ import org.elasticsearch.common.transport.InetSocketTransportAddress; import org.junit.After; import org.junit.Before; import org.junit.Test; -import org.sonar.process.Process; +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.File; import java.io.IOException; import java.lang.management.ManagementFactory; import java.net.ServerSocket; -import java.net.SocketException; import java.util.Properties; import static org.fest.assertions.Assertions.assertThat; import static org.junit.Assert.fail; -public class ElasticSearchTest { +public class SearchServerTest { File tempDirectory; - ElasticSearch elasticSearch; + SearchServer searchServer; int freePort; int freeESPort; @@ -64,7 +65,6 @@ public class ElasticSearchTest { socket.close(); } - @After public void tearDown() throws MBeanRegistrationException, InstanceNotFoundException { resetMBeanServer(); @@ -73,32 +73,32 @@ public class ElasticSearchTest { private void resetMBeanServer() throws MBeanRegistrationException, InstanceNotFoundException { try { MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); - mbeanServer.unregisterMBean(Process.objectNameFor("ES")); + mbeanServer.unregisterMBean(JmxUtils.objectName("ES")); } catch (Exception e) { e.printStackTrace(); } } @Test - public void can_connect() throws SocketException { + public void can_connect() throws Exception { Properties properties = new Properties(); - properties.setProperty(Process.NAME_PROPERTY, "ES"); + properties.setProperty(MonitoredProcess.NAME_PROPERTY, "ES"); properties.setProperty("sonar.path.data", tempDirectory.getAbsolutePath()); properties.setProperty("sonar.path.logs", tempDirectory.getAbsolutePath()); - properties.setProperty(ElasticSearch.ES_PORT_PROPERTY, Integer.toString(freeESPort)); - properties.setProperty(ElasticSearch.ES_CLUSTER_PROPERTY, "sonarqube"); + properties.setProperty(SearchServer.ES_PORT_PROPERTY, Integer.toString(freeESPort)); + properties.setProperty(SearchServer.ES_CLUSTER_PROPERTY, "sonarqube"); - elasticSearch = new ElasticSearch(new Props(properties)); + searchServer = new SearchServer(new Props(properties)); new Thread(new Runnable() { @Override public void run() { - elasticSearch.start(); + searchServer.start(); } }).start(); - assertThat(elasticSearch.isReady()).isFalse(); + assertThat(searchServer.isReady()).isFalse(); int count = 0; - while (!elasticSearch.isReady() && count < 100) { + while (!searchServer.isReady() && count < 100) { try { Thread.sleep(500); } catch (InterruptedException e) { @@ -114,13 +114,11 @@ public class ElasticSearchTest { TransportClient client = new TransportClient(settings) .addTransportAddress(new InetSocketTransportAddress("localhost", freeESPort)); - // 0 assert that we have a OK cluster available assertThat(client.admin().cluster().prepareClusterStats().get().getStatus()).isEqualTo(ClusterHealthStatus.GREEN); - // 2 assert that we can shut down ES - elasticSearch.terminate(); + searchServer.terminate(); try { client.admin().cluster().prepareClusterStats().get().getStatus(); fail(); 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 32f323c96ee..596541d3c3d 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,15 +19,17 @@ */ package org.sonar.server.app; -import org.apache.catalina.LifecycleException; import org.apache.catalina.connector.Connector; 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.Terminatable; import java.io.File; -class EmbeddedTomcat { +class EmbeddedTomcat implements Terminatable { private final Props props; private Tomcat tomcat = null; @@ -64,60 +66,22 @@ class EmbeddedTomcat { Logging.configure(tomcat, props); Connectors.configure(tomcat, props); Webapp.configure(tomcat, props); + ProcessUtils.addSelfShutdownHook(this); tomcat.start(); - addShutdownHook(); 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(); } - stop(); } private File tomcatBasedir() { return new File(props.of("sonar.path.temp"), "tomcat"); } - private void addShutdownHook() { - hook = new Thread() { - @Override - public void run() { - EmbeddedTomcat.this.doStop(); - } - }; - Runtime.getRuntime().addShutdownHook(hook); - } - - - void stop() { - removeShutdownHook(); - doStop(); - } - - private synchronized void doStop() { - try { - if (tomcat != null && !stopping) { - stopping = true; - tomcat.stop(); - tomcat.destroy(); - } - tomcat = null; - stopping = false; - ready = false; - FileUtils.deleteQuietly(tomcatBasedir()); - - } catch (LifecycleException e) { - throw new IllegalStateException("Fail to stop web server", e); - } - } - - private void removeShutdownHook() { - if (hook != null && !hook.isAlive()) { - Runtime.getRuntime().removeShutdownHook(hook); - hook = null; - } - } - boolean isReady() { return ready && tomcat != null; } @@ -129,4 +93,21 @@ class EmbeddedTomcat { } return -1; } + + @Override + public void terminate() { + if (tomcat != null && !stopping) { + try { + stopping = true; + tomcat.stop(); + tomcat.destroy(); + } catch (Exception e) { + LoggerFactory.getLogger(EmbeddedTomcat.class).error("Fail to stop web service", e); + } + } + tomcat = null; + stopping = false; + ready = false; + FileUtils.deleteQuietly(tomcatBasedir()); + } } diff --git a/server/sonar-server/src/main/java/org/sonar/server/app/ServerProcess.java b/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java index 6651f5355d3..77b95f945b1 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/app/ServerProcess.java +++ b/server/sonar-server/src/main/java/org/sonar/server/app/WebServer.java @@ -21,19 +21,20 @@ package org.sonar.server.app; import org.slf4j.LoggerFactory; import org.sonar.process.ConfigurationUtils; +import org.sonar.process.MonitoredProcess; import org.sonar.process.Props; -public class ServerProcess extends org.sonar.process.Process { +public class WebServer extends MonitoredProcess { private final EmbeddedTomcat tomcat; - ServerProcess(Props props) { + WebServer(Props props) throws Exception { super(props); this.tomcat = new EmbeddedTomcat(props); } @Override - public void onStart() { + protected void doStart() { try { tomcat.start(); } catch (Exception e) { @@ -44,18 +45,18 @@ public class ServerProcess extends org.sonar.process.Process { } @Override - public void onTerminate() { - tomcat.stop(); + protected void doTerminate() { + tomcat.terminate(); } @Override - public boolean isReady() { + protected boolean doIsReady() { return tomcat.isReady(); } - public static void main(String[] args) { + public static void main(String[] args) throws Exception { Props props = ConfigurationUtils.loadPropsFromCommandLineArgs(args); Logging.init(props); - new ServerProcess(props).start(); + new WebServer(props).start(); } } 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 fea8916ccbe..85b674157b1 100644 --- a/sonar-application/src/main/java/org/sonar/application/App.java +++ b/sonar-application/src/main/java/org/sonar/application/App.java @@ -21,125 +21,78 @@ package org.sonar.application; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.sonar.process.JmxUtils; import org.sonar.process.Monitor; -import org.sonar.process.Process; import org.sonar.process.ProcessMXBean; +import org.sonar.process.ProcessUtils; import org.sonar.process.ProcessWrapper; -import javax.management.InstanceAlreadyExistsException; -import javax.management.MBeanRegistrationException; -import javax.management.MBeanServer; -import javax.management.NotCompliantMBeanException; -import java.lang.management.ManagementFactory; - public class App implements ProcessMXBean { - static final String PROCESS_NAME = "SonarQube"; - static final String SONAR_WEB_PROCESS = "web"; static final String SONAR_SEARCH_PROCESS = "search"; - private final Installation installation; - private Monitor monitor; + + private final Monitor monitor = new Monitor(); private ProcessWrapper elasticsearch; private ProcessWrapper server; public App(Installation installation) throws Exception { this.installation = installation; - - Thread shutdownHook = new Thread(new Runnable() { - @Override - public void run() { - terminate(); - } - }); - Runtime.getRuntime().addShutdownHook(shutdownHook); - - MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer(); - try { - mbeanServer.registerMBean(this, Process.objectNameFor(PROCESS_NAME)); - } catch (InstanceAlreadyExistsException e) { - throw new IllegalStateException("Process already exists in current JVM", e); - } catch (MBeanRegistrationException e) { - throw new IllegalStateException("Could not register process as MBean", e); - } catch (NotCompliantMBeanException e) { - throw new IllegalStateException("Process is not a compliant MBean", e); - } - - monitor = new Monitor(); + JmxUtils.registerMBean(this, "SonarQube"); + ProcessUtils.addSelfShutdownHook(this); } - public void start() { - - Logger logger = LoggerFactory.getLogger(getClass()); - - logger.info("Starting search server"); - elasticsearch = new ProcessWrapper(SONAR_SEARCH_PROCESS) - .setWorkDir(installation.homeDir()) - .setJmxPort(Integer.parseInt(installation.prop(DefaultSettings.ES_JMX_PORT_KEY))) - .addJavaOpts(installation.prop(DefaultSettings.ES_JAVA_OPTS_KEY)) - .addJavaOpts(String.format("-Djava.io.tmpdir=%s", installation.tempDir().getAbsolutePath())) - .addJavaOpts(String.format("-D%s=%s", DefaultSettings.PATH_LOGS_KEY, installation.logsDir().getAbsolutePath())) - .setClassName("org.sonar.search.ElasticSearch") - .setProperties(installation.props().encryptedProperties()) - .addClasspath(installation.starPath("lib/common")) - .addClasspath(installation.starPath("lib/search")) - .execute(); - monitor.registerProcess(elasticsearch); - logger.info("Search server is ready"); - - logger.info("Starting web server"); - server = new ProcessWrapper(SONAR_WEB_PROCESS) - .setWorkDir(installation.homeDir()) - .setJmxPort(Integer.parseInt(installation.prop(DefaultSettings.WEB_JMX_PORT_KEY))) - .addJavaOpts(installation.prop(DefaultSettings.WEB_JAVA_OPTS_KEY)) - .addJavaOpts(DefaultSettings.WEB_JAVA_OPTS_APPENDED_VAL) - .addJavaOpts(String.format("-Djava.io.tmpdir=%s", installation.tempDir().getAbsolutePath())) - .addJavaOpts(String.format("-D%s=%s", DefaultSettings.PATH_LOGS_KEY, installation.logsDir().getAbsolutePath())) - .setClassName("org.sonar.server.app.ServerProcess") - .setProperties(installation.props().encryptedProperties()) - .addClasspath(installation.starPath("extensions/jdbc-driver/mysql")) - .addClasspath(installation.starPath("extensions/jdbc-driver/mssql")) - .addClasspath(installation.starPath("extensions/jdbc-driver/oracle")) - .addClasspath(installation.starPath("extensions/jdbc-driver/postgresql")) - .addClasspath(installation.starPath("lib/common")) - .addClasspath(installation.starPath("lib/server")) - .execute(); - monitor.registerProcess(server); - logger.info("Web server is ready"); - - monitor.start(); - + public void start() throws InterruptedException { try { - monitor.join(); - } catch (InterruptedException e) { - // TODO ignore ? - + Logger logger = LoggerFactory.getLogger(getClass()); + monitor.start(); + + elasticsearch = new ProcessWrapper(SONAR_SEARCH_PROCESS) + .setWorkDir(installation.homeDir()) + .setJmxPort(Integer.parseInt(installation.prop(DefaultSettings.ES_JMX_PORT_KEY))) + .addJavaOpts(installation.prop(DefaultSettings.ES_JAVA_OPTS_KEY)) + .addJavaOpts(String.format("-Djava.io.tmpdir=%s", installation.tempDir().getAbsolutePath())) + .addJavaOpts(String.format("-Dsonar.path.logs=%s", installation.logsDir().getAbsolutePath())) + .setClassName("org.sonar.search.SearchServer") + .setProperties(installation.props().encryptedProperties()) + .addClasspath(installation.starPath("lib/common")) + .addClasspath(installation.starPath("lib/search")); + if (elasticsearch.execute()) { + monitor.registerProcess(elasticsearch); + if (elasticsearch.waitForReady()) { + logger.info("Search server is ready"); + + server = new ProcessWrapper(SONAR_WEB_PROCESS) + .setWorkDir(installation.homeDir()) + .setJmxPort(Integer.parseInt(installation.prop(DefaultSettings.WEB_JMX_PORT_KEY))) + .addJavaOpts(installation.prop(DefaultSettings.WEB_JAVA_OPTS_KEY)) + .addJavaOpts(DefaultSettings.WEB_JAVA_OPTS_APPENDED_VAL) + .addJavaOpts(String.format("-Djava.io.tmpdir=%s", installation.tempDir().getAbsolutePath())) + .setClassName("org.sonar.server.app.WebServer") + .setProperties(installation.props().encryptedProperties()) + .addClasspath(installation.starPath("extensions/jdbc-driver/mysql")) + .addClasspath(installation.starPath("extensions/jdbc-driver/mssql")) + .addClasspath(installation.starPath("extensions/jdbc-driver/oracle")) + .addClasspath(installation.starPath("extensions/jdbc-driver/postgresql")) + .addClasspath(installation.starPath("lib/common")) + .addClasspath(installation.starPath("lib/server")); + if (server.execute()) { + monitor.registerProcess(server); + if (server.waitForReady()) { + logger.info("Web server is ready"); + monitor.join(); + } + } + } + } } finally { - logger.debug("Closing App because monitor is gone."); terminate(); } } @Override - public void terminate() { - Logger logger = LoggerFactory.getLogger(getClass()); - - if (monitor != null) { - monitor.interrupt(); - monitor = null; - if (elasticsearch != null) { - elasticsearch.terminate(); - } - if (server != null) { - server.terminate(); - } - logger.info("Stopping SonarQube main process"); - } - } - - @Override public boolean isReady() { return monitor.isAlive(); } @@ -149,6 +102,18 @@ public class App implements ProcessMXBean { return System.currentTimeMillis(); } + @Override + public void terminate() { + monitor.terminate(); + monitor.interrupt(); + if (server != null) { + server.terminate(); + } + if (elasticsearch != null) { + elasticsearch.terminate(); + } + } + public static void main(String[] args) throws Exception { Installation installation = new Installation(); new AppLogging().configure(installation); diff --git a/sonar-application/src/test/java/org/sonar/application/AppTest.java b/sonar-application/src/test/java/org/sonar/application/AppTest.java index de1eb77584c..7bfba4f2562 100644 --- a/sonar-application/src/test/java/org/sonar/application/AppTest.java +++ b/sonar-application/src/test/java/org/sonar/application/AppTest.java @@ -22,22 +22,14 @@ package org.sonar.application; import org.apache.commons.io.FileUtils; 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.Process; -import org.sonar.process.ProcessMXBean; -import javax.management.MBeanServer; -import javax.management.ObjectName; import java.io.File; -import java.lang.management.ManagementFactory; import static org.fest.assertions.Assertions.assertThat; import static org.junit.Assert.fail; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; public class AppTest { |