/* * SonarQube * Copyright (C) 2009-2017 SonarSource SA * mailto:info AT sonarsource DOT com * * This program 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. * * This program 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.process; import java.util.ArrayList; import java.util.List; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.sonar.process.ProcessId; import static java.lang.String.format; import static java.util.Objects.requireNonNull; public class SQProcess { public static final long DEFAULT_WATCHER_DELAY_MS = 500L; private static final Logger LOG = LoggerFactory.getLogger(SQProcess.class); private final ProcessId processId; private final Lifecycle lifecycle; private final List<ProcessEventListener> eventListeners; private final long watcherDelayMs; private ProcessMonitor process; private StreamGobbler gobbler; private final StopWatcher stopWatcher; private final EventWatcher eventWatcher; // keep flag so that the operational event is sent only once // to listeners private final AtomicBoolean operational = new AtomicBoolean(false); private SQProcess(Builder builder) { this.processId = requireNonNull(builder.processId, "processId can't be null"); this.lifecycle = new Lifecycle(this.processId, builder.lifecycleListeners); this.eventListeners = builder.eventListeners; this.watcherDelayMs = builder.watcherDelayMs; this.stopWatcher = new StopWatcher(); this.eventWatcher = new EventWatcher(); } public boolean start(Supplier<ProcessMonitor> commandLauncher) { if (!lifecycle.tryToMoveTo(Lifecycle.State.STARTING)) { // has already been started return false; } try { this.process = commandLauncher.get(); } catch (RuntimeException e) { LOG.error(format("Fail to launch process [%s]", processId.getKey()), e); lifecycle.tryToMoveTo(Lifecycle.State.STOPPED); throw e; } this.gobbler = new StreamGobbler(process.getInputStream(), processId.getKey()); this.gobbler.start(); this.stopWatcher.start(); this.eventWatcher.start(); // Could be improved by checking the status "up" in shared memory. // Not a problem so far as this state is not used by listeners. lifecycle.tryToMoveTo(Lifecycle.State.STARTED); return true; } public ProcessId getProcessId() { return processId; } Lifecycle.State getState() { return lifecycle.getState(); } /** * Sends kill signal and awaits termination. No guarantee that process is gracefully terminated (=shutdown hooks * executed). It depends on OS. */ public void stop(long timeout, TimeUnit timeoutUnit) { if (lifecycle.tryToMoveTo(Lifecycle.State.STOPPING)) { stopGracefully(timeout, timeoutUnit); if (process != null && process.isAlive()) { LOG.info("{} failed to stop in a timely fashion. Killing it.", processId.getKey()); } // enforce stop and clean-up even if process has been gracefully stopped stopForcibly(); } else { // already stopping or stopped waitForDown(); } } private void waitForDown() { while (process != null && process.isAlive()) { try { process.waitFor(); } catch (InterruptedException ignored) { // ignore, waiting for process to stop Thread.currentThread().interrupt(); } } } private void stopGracefully(long timeout, TimeUnit timeoutUnit) { if (process == null) { return; } try { // request graceful stop process.askForStop(); process.waitFor(timeout, timeoutUnit); } catch (InterruptedException e) { // can't wait for the termination of process. Let's assume it's down. LOG.warn(format("Interrupted while stopping process %s", processId), e); Thread.currentThread().interrupt(); } catch (Throwable e) { LOG.error("Can not ask for graceful stop of process " + processId, e); } } public void stopForcibly() { eventWatcher.interrupt(); stopWatcher.interrupt(); if (process != null) { process.destroyForcibly(); waitForDown(); process.closeStreams(); } if (gobbler != null) { StreamGobbler.waitUntilFinish(gobbler); gobbler.interrupt(); } lifecycle.tryToMoveTo(Lifecycle.State.STOPPED); } void refreshState() { if (process.isAlive()) { if (!operational.get() && process.isOperational()) { operational.set(true); eventListeners.forEach(l -> l.onProcessEvent(processId, ProcessEventListener.Type.OPERATIONAL)); } if (process.askedForRestart()) { process.acknowledgeAskForRestart(); eventListeners.forEach(l -> l.onProcessEvent(processId, ProcessEventListener.Type.ASK_FOR_RESTART)); } } else { stopForcibly(); } } @Override public String toString() { return format("Process[%s]", processId.getKey()); } /** * 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> */ private class StopWatcher extends Thread { StopWatcher() { // this name is different than Thread#toString(), which includes name, priority // and thread group // -> do not override toString() super(format("StopWatcher[%s]", processId.getKey())); } @Override public void run() { try { process.waitFor(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // stop watching process } stopForcibly(); } } private class EventWatcher extends Thread { EventWatcher() { // this name is different than Thread#toString(), which includes name, priority // and thread group // -> do not override toString() super(format("EventWatcher[%s]", processId.getKey())); } @Override public void run() { try { while (process.isAlive()) { refreshState(); Thread.sleep(watcherDelayMs); } } catch (InterruptedException e) { // request to stop watching process. To avoid unexpected behaviors // the process is stopped. Thread.currentThread().interrupt(); stopForcibly(); } } } public static Builder builder(ProcessId processId) { return new Builder(processId); } public static class Builder { private final ProcessId processId; private final List<ProcessEventListener> eventListeners = new ArrayList<>(); private final List<ProcessLifecycleListener> lifecycleListeners = new ArrayList<>(); private long watcherDelayMs = DEFAULT_WATCHER_DELAY_MS; private Builder(ProcessId processId) { this.processId = processId; } public Builder addEventListener(ProcessEventListener listener) { this.eventListeners.add(listener); return this; } public Builder addProcessLifecycleListener(ProcessLifecycleListener listener) { this.lifecycleListeners.add(listener); return this; } /** * Default delay is {@link #DEFAULT_WATCHER_DELAY_MS} */ public Builder setWatcherDelayMs(long l) { this.watcherDelayMs = l; return this; } public SQProcess build() { return new SQProcess(this); } } }