001/*
002 * SonarQube, open source software quality management tool.
003 * Copyright (C) 2008-2014 SonarSource
004 * mailto:contact AT sonarsource DOT com
005 *
006 * SonarQube is free software; you can redistribute it and/or
007 * modify it under the terms of the GNU Lesser General Public
008 * License as published by the Free Software Foundation; either
009 * version 3 of the License, or (at your option) any later version.
010 *
011 * SonarQube is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014 * Lesser General Public License for more details.
015 *
016 * You should have received a copy of the GNU Lesser General Public License
017 * along with this program; if not, write to the Free Software Foundation,
018 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
019 */
020package org.sonar.process.monitor;
021
022import org.slf4j.LoggerFactory;
023import org.sonar.process.Lifecycle;
024import org.sonar.process.Lifecycle.State;
025import org.sonar.process.ProcessCommands;
026import org.sonar.process.SystemExit;
027
028import java.util.List;
029import java.util.concurrent.CopyOnWriteArrayList;
030
031public class Monitor {
032
033  private final List<ProcessRef> processes = new CopyOnWriteArrayList<ProcessRef>();
034  private final TerminatorThread terminator;
035  private final JavaProcessLauncher launcher;
036  private final Lifecycle lifecycle = new Lifecycle();
037
038  private final SystemExit systemExit;
039  private Thread shutdownHook = new Thread(new MonitorShutdownHook(), "Monitor Shutdown Hook");
040
041  // used by awaitStop() to block until all processes are shutdown
042  private final List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<WatcherThread>();
043  static int nextProcessId = 0;
044
045  Monitor(JavaProcessLauncher launcher, SystemExit exit, TerminatorThread terminator) {
046    this.launcher = launcher;
047    this.terminator = terminator;
048    this.systemExit = exit;
049  }
050
051  public static Monitor create() {
052    Timeouts timeouts = new Timeouts();
053    return new Monitor(new JavaProcessLauncher(timeouts), new SystemExit(), new TerminatorThread(timeouts));
054  }
055
056  /**
057   * Starts commands and blocks current thread until all processes are in state {@link State#STARTED}.
058   * @throws java.lang.IllegalArgumentException if commands list is empty
059   * @throws java.lang.IllegalStateException if already started or if at least one process failed to start. In this case
060   *   all processes are terminated. No need to execute {@link #stop()}
061   */
062  public void start(List<JavaCommand> commands) {
063    if (commands.isEmpty()) {
064      throw new IllegalArgumentException("At least one command is required");
065    }
066
067    if (!lifecycle.tryToMoveTo(State.STARTING)) {
068      throw new IllegalStateException("Can not start multiple times");
069    }
070
071    // intercepts CTRL-C
072    Runtime.getRuntime().addShutdownHook(shutdownHook);
073
074    for (JavaCommand command : commands) {
075      try {
076        ProcessRef processRef = launcher.launch(command);
077        monitor(processRef);
078      } catch (RuntimeException e) {
079        // fail to start or to monitor
080        stop();
081        throw e;
082      }
083    }
084
085    if (!lifecycle.tryToMoveTo(State.STARTED)) {
086      // stopping or stopped during startup, for instance :
087      // 1. A is started
088      // 2. B starts
089      // 3. A crashes while B is starting
090      // 4. if B was not monitored during Terminator execution, then it's an alive orphan
091      stop();
092      throw new IllegalStateException("Stopped during startup");
093    }
094  }
095
096  private void monitor(ProcessRef processRef) {
097    // physically watch if process is alive
098    WatcherThread watcherThread = new WatcherThread(processRef, this);
099    watcherThread.start();
100    watcherThreads.add(watcherThread);
101
102    processes.add(processRef);
103
104    // wait for process to be ready (accept requests or so on)
105    processRef.waitForReady();
106
107    LoggerFactory.getLogger(getClass()).info(String.format("%s is up", processRef));
108  }
109
110  /**
111   * Blocks until all processes are terminated
112   */
113  public void awaitTermination() {
114    for (WatcherThread watcherThread : watcherThreads) {
115      while (watcherThread.isAlive()) {
116        try {
117          watcherThread.join();
118        } catch (InterruptedException ignored) {
119          // ignore, stop blocking
120        }
121      }
122    }
123  }
124
125  /**
126   * Blocks until all processes are terminated.
127   */
128  public void stop() {
129    stopAsync();
130    try {
131      terminator.join();
132    } catch (InterruptedException ignored) {
133      // stop blocking and exiting
134    }
135    // safeguard if TerminatorThread is buggy
136    lifecycle.tryToMoveTo(State.STOPPED);
137    systemExit.exit(0);
138  }
139
140  /**
141   * Asks for processes termination and returns without blocking until termination.
142   */
143  public void stopAsync() {
144    if (lifecycle.tryToMoveTo(State.STOPPING)) {
145      terminator.setProcesses(processes);
146      terminator.start();
147    }
148  }
149
150  public State getState() {
151    return lifecycle.getState();
152  }
153
154  Thread getShutdownHook() {
155    return shutdownHook;
156  }
157
158  private class MonitorShutdownHook implements Runnable {
159    @Override
160    public void run() {
161      systemExit.setInShutdownHook();
162      // blocks until everything is corrected terminated
163      stop();
164    }
165  }
166
167  public static int getNextProcessId() {
168    if (nextProcessId >= ProcessCommands.getMaxProcesses()) {
169      throw new IllegalStateException("The maximum number of processes launched has been reached " + ProcessCommands.getMaxProcesses());
170    }
171    return nextProcessId++;
172  }
173}