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.SystemExit;
026
027import java.util.List;
028import java.util.concurrent.CopyOnWriteArrayList;
029
030public class Monitor {
031
032  private final List<ProcessRef> processes = new CopyOnWriteArrayList<ProcessRef>();
033  private final TerminatorThread terminator;
034  private final JavaProcessLauncher launcher;
035  private final Lifecycle lifecycle = new Lifecycle();
036
037  private final SystemExit systemExit;
038  private Thread shutdownHook = new Thread(new MonitorShutdownHook(), "Monitor Shutdown Hook");
039
040  // used by awaitStop() to block until all processes are shutdown
041  private final List<WatcherThread> watcherThreads = new CopyOnWriteArrayList<WatcherThread>();
042
043  Monitor(JavaProcessLauncher launcher, SystemExit exit, TerminatorThread terminator) {
044    this.launcher = launcher;
045    this.terminator = terminator;
046    this.systemExit = exit;
047  }
048
049  public static Monitor create() {
050    Timeouts timeouts = new Timeouts();
051    return new Monitor(new JavaProcessLauncher(timeouts), new SystemExit(), new TerminatorThread(timeouts));
052  }
053
054  /**
055   * Starts commands and blocks current thread until all processes are in state {@link State#STARTED}.
056   * @throws java.lang.IllegalArgumentException if commands list is empty
057   * @throws java.lang.IllegalStateException if already started or if at least one process failed to start. In this case
058   *   all processes are terminated. No need to execute {@link #stop()}
059   */
060  public void start(List<JavaCommand> commands) {
061    if (commands.isEmpty()) {
062      throw new IllegalArgumentException("At least one command is required");
063    }
064
065    if (!lifecycle.tryToMoveTo(State.STARTING)) {
066      throw new IllegalStateException("Can not start multiple times");
067    }
068
069    // intercepts CTRL-C
070    Runtime.getRuntime().addShutdownHook(shutdownHook);
071
072    for (JavaCommand command : commands) {
073      try {
074        ProcessRef processRef = launcher.launch(command);
075        monitor(command, processRef);
076      } catch (RuntimeException e) {
077        // fail to start or to monitor
078        stop();
079        throw e;
080      }
081    }
082
083    if (!lifecycle.tryToMoveTo(State.STARTED)) {
084      // stopping or stopped during startup, for instance :
085      // 1. A is started
086      // 2. B starts
087      // 3. A crashes while B is starting
088      // 4. if B was not monitored during Terminator execution, then it's an alive orphan
089      stop();
090      throw new IllegalStateException("Stopped during startup");
091    }
092  }
093
094  private void monitor(JavaCommand command, ProcessRef processRef) {
095    // physically watch if process is alive
096    WatcherThread watcherThread = new WatcherThread(processRef, this);
097    watcherThread.start();
098    watcherThreads.add(watcherThread);
099
100    processes.add(processRef);
101
102    // wait for process to be ready (accept requests or so on)
103    processRef.waitForReady();
104
105    LoggerFactory.getLogger(getClass()).info(String.format("%s is up", processRef));
106  }
107
108  /**
109   * Blocks until all processes are terminated
110   */
111  public void awaitTermination() {
112    for (WatcherThread watcherThread : watcherThreads) {
113      while (watcherThread.isAlive()) {
114        try {
115          watcherThread.join();
116        } catch (InterruptedException ignored) {
117          // ignore, stop blocking
118        }
119      }
120    }
121  }
122
123  /**
124   * Blocks until all processes are terminated.
125   */
126  public void stop() {
127    stopAsync();
128    try {
129      terminator.join();
130    } catch (InterruptedException ignored) {
131      // stop blocking and exiting
132    }
133    // safeguard if TerminatorThread is buggy
134    lifecycle.tryToMoveTo(State.STOPPED);
135    systemExit.exit(0);
136  }
137
138  /**
139   * Asks for processes termination and returns without blocking until termination.
140   */
141  public void stopAsync() {
142    if (lifecycle.tryToMoveTo(State.STOPPING)) {
143      terminator.setProcesses(processes);
144      terminator.start();
145    }
146  }
147
148  public State getState() {
149    return lifecycle.getState();
150  }
151
152  Thread getShutdownHook() {
153    return shutdownHook;
154  }
155
156  private class MonitorShutdownHook implements Runnable {
157    @Override
158    public void run() {
159      systemExit.setInShutdownHook();
160      // blocks until everything is corrected terminated
161      stop();
162    }
163  }
164}