WaarpShutdownHook.java

/*
 * This file is part of Waarp Project (named also Waarp or GG).
 *
 *  Copyright (c) 2019, Waarp SAS, and individual contributors by the @author
 *  tags. See the COPYRIGHT.txt in the distribution for a full listing of
 * individual contributors.
 *
 *  All Waarp Project is free software: you can redistribute it and/or
 * modify it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or (at your
 * option) any later version.
 *
 * Waarp 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 General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License along with
 * Waarp . If not, see <http://www.gnu.org/licenses/>.
 */
package org.waarp.common.utility;

import org.waarp.common.future.WaarpFuture;
import org.waarp.common.logging.SysErrLogger;
import org.waarp.common.logging.WaarpLogger;
import org.waarp.common.logging.WaarpLoggerFactory;

import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Timer;
import java.util.TimerTask;

/**
 *
 */
public abstract class WaarpShutdownHook extends Thread {
  /**
   * Internal Logger
   */
  private static final WaarpLogger logger =
      WaarpLoggerFactory.getLogger(WaarpShutdownHook.class);
  /**
   * Sun property pointing the main class and its arguments. Might not be
   * defined on non Hotspot VM
   * implementations.
   */
  private static final String SUN_JAVA_COMMAND = "sun.java.command";
  /**
   * Thread for ShutdownHook
   */
  public static WaarpShutdownHook shutdownHook;
  /**
   * Set if the program is in shutdown
   */
  private static volatile boolean shutdown;
  /**
   * Set if the program will start shutdown process
   */
  private static volatile boolean shutdownStarted;
  /**
   * Set if the program is in shutdown
   */
  private static volatile boolean immediate;
  /**
   * Set if the Handler is initialized
   */
  private static boolean initialized;
  /**
   * Is the shutdown finished
   */
  private static boolean isShutdownOver;
  private static String applArgs;
  private static volatile boolean shouldRestart;
  private ShutdownConfiguration shutdownConfiguration;

  protected WaarpShutdownHook(final ShutdownConfiguration configuration) {
    if (initialized) {
      shutdownHook.shutdownConfiguration = configuration;
      setName("WaarpShutdownHook");
      setDaemon(true);
      shutdownHook = this;
      shutdownConfiguration = configuration;
      return;
    }
    shutdownConfiguration = configuration;
    setName("WaarpShutdownHook");
    setDaemon(true);
    shutdownHook = this;
    initialized = true;
  }

  /**
   * @return the current ShutdownConfiguration
   */
  public final ShutdownConfiguration getShutdownConfiguration() {
    return shutdownConfiguration;
  }

  /**
   * For Server part
   */
  public static void addShutdownHook() {
    if (shutdownHook != null) {
      Runtime.getRuntime().addShutdownHook(shutdownHook);
    }
  }

  /**
   * Says if the Process is currently in shutdown
   *
   * @return True if already in shutdown
   */
  public static boolean isInShutdown() {
    return shutdown;
  }

  /**
   * @return True if the Shutdown process will start soon
   */
  public static boolean isShutdownStarting() {
    return shutdownStarted;
  }

  /**
   * To specify that shutdown will soon start
   */
  public static void shutdownWillStart() {
    shutdownStarted = true;
  }

  /**
   * This function is the top function to be called when the process is to be
   * shutdown.
   *
   * @param immediateSet
   */
  public static void terminate(final boolean immediateSet) {
    if (immediateSet) {
      immediate = true;
    }
    if (shutdownHook != null) {
      removeShutdownHook();
      terminate();
      shutdownHook = null;
    } else {
      logger.error("No ShutdownHook setup");
      //FBGEXIT DetectionUtils.SystemExit(1)
    }
  }

  /**
   * For Server part
   */
  public static void removeShutdownHook() {
    if (shutdownHook != null) {
      Runtime.getRuntime().removeShutdownHook(shutdownHook);
    }
  }

  /**
   * Intermediary exit function
   */
  private static void terminate() {
    shutdownStarted = true;
    shutdown = true;
    if (isShutdownOver || shutdownHook == null) {
      shutdown = false;
      shutdownStarted = false;
      isShutdownOver = false;
      initialized = false;
      return;
    }
    if (immediate) {
      shutdownHook.exitService();
      // Force exit!
      try {
        Thread.sleep(shutdownHook.shutdownConfiguration.timeout / 2);
      } catch (final InterruptedException e) {//NOSONAR
        SysErrLogger.FAKE_LOGGER.ignoreLog(e);
      }
      if (logger.isDebugEnabled()) {
        final Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
        for (final Entry<Thread, StackTraceElement[]> entry : map.entrySet()) {
          printStackTrace(entry.getKey(), entry.getValue());
        }
      }
      isShutdownOver = true;
      logger.info("Should restart? {}", isRestart());
      try {
        restartApplication();
      } catch (final IOException e1) {
        SysErrLogger.FAKE_LOGGER.ignoreLog(e1);
      }
      shutdownHook.serviceStopped();
      SysErrLogger.FAKE_LOGGER.syserr("Halt System");
      try {
        Thread.sleep(WaarpNettyUtil.SIMPLE_DELAY_MS);
      } catch (final InterruptedException e) {//NOSONAR
        SysErrLogger.FAKE_LOGGER.ignoreLog(e);
      }
      //FBGEXIT DetectionUtils.SystemExit(0)
    } else {
      shutdownHook.launchFinalExit();
      immediate = true;
      shutdownHook.exitService();
      isShutdownOver = true;
      shutdownHook.serviceStopped();
      logger.info("Should restart? {}", isRestart());
      try {
        restartApplication();
      } catch (final IOException e1) {
        SysErrLogger.FAKE_LOGGER.syserr(e1);
      }
      logger.info("Exit System");
      SysErrLogger.FAKE_LOGGER.syserr("Exit System");
    }
    shutdown = false;
    shutdownStarted = false;
    isShutdownOver = false;
    initialized = false;
  }

  /**
   * Real exit function
   */
  protected abstract void exitService();

  /**
   * Print stack trace
   *
   * @param thread
   * @param stacks
   */
  private static void printStackTrace(final Thread thread,
                                      final StackTraceElement[] stacks) {
    SysErrLogger.FAKE_LOGGER.syserrNoLn(thread + " : ");
    for (int i = 0; i < stacks.length - 1; i++) {
      SysErrLogger.FAKE_LOGGER.syserrNoLn(stacks[i] + " ");
    }
    if (stacks.length >= 1) {
      SysErrLogger.FAKE_LOGGER.syserr(stacks[stacks.length - 1]);
    } else {
      SysErrLogger.FAKE_LOGGER.syserr();
    }
  }

  /**
   * @return True if the shutdown should be followed by a restart
   */
  public static boolean isRestart() {
    return shouldRestart;
  }

  /**
   * Restart the application using the preset applArgs and computing the
   * jvmArgs. execute the command in a
   * shutdown hook, to be sure that all the resources have been disposed
   * before
   * restarting the application
   *
   * @throws IOException
   */
  private static void restartApplication() throws IOException {
    if (shouldRestart) {
      try {
        // java binary
        final String java = System.getProperty("java.home") + "/bin/java";
        // vm arguments
        final List<String> vmArguments =
            ManagementFactory.getRuntimeMXBean().getInputArguments();
        final StringBuilder vmArgsOneLine = new StringBuilder();
        for (final String arg : vmArguments) {
          // if it's the agent argument : we ignore it otherwise the
          // address of the old application and the new one will be in conflict
          if (!arg.contains("-agentlib")) {
            vmArgsOneLine.append(arg).append(' ');
          }
        }
        // init the command to execute, add the vm args
        final StringBuilder cmd;
        if (DetectionUtils.isWindows()) {
          cmd = new StringBuilder('"' + java + "\" " + vmArgsOneLine);
        } else {
          cmd = new StringBuilder(java + ' ' + vmArgsOneLine);
        }

        if (applArgs == null) {
          applArgs = getArgs();
        }
        if (applArgs == null) {
          // big issue then
          SysErrLogger.FAKE_LOGGER.syserr("Cannot restart!");
          // something went wrong
          throw new IOException(
              "Error while trying to restart the " + "application");
        }
        cmd.append(applArgs);
        logger.debug("Should restart with:\n{}", cmd);
        logger.warn("Should restart");
        Runtime.getRuntime().exec(cmd.toString()); //NOSONAR
      } catch (final Throwable e) {
        // something went wrong
        throw new IOException("Error while trying to restart the application",
                              e);
      }
    }
  }

  private boolean serviceStopped() {
    if (shutdownConfiguration.serviceFuture != null) {
      logger.info("Service will be stopped");
      shutdownConfiguration.serviceFuture.setSuccess();
      return true;
    }
    return false;
  }

  /**
   * Extra call to ensure exit after long delay
   */
  public final void launchFinalExit() {
    if (WaarpSystemUtil.isJunit()) {
      return;
    }
    final Timer timer = new Timer("WaarpFinalExit", true);
    final ShutdownTimerTask timerTask = new ShutdownTimerTask();
    timer.schedule(timerTask, shutdownConfiguration.timeout * 4);
  }

  /**
   * Try to return the application arguments (for Oracle VM)
   *
   * @return null if it cannot
   */
  private static String getArgs() {
    final String test = System.getProperty(SUN_JAVA_COMMAND);
    if (ParametersChecker.isNotEmpty(test)) {
      // compute args directly
      // program main and program arguments
      final String[] mainCommand = test.split(" ");
      // program main is a jar
      final StringBuilder args = new StringBuilder();
      if (mainCommand[0].endsWith(".jar")) {
        // if it's a jar, add -jar mainJar
        args.append("-jar ").append(new File(mainCommand[0]).getPath());
      } else {
        // else it's a .class, add the classpath and mainClass
        args.append("-cp \"").append(System.getProperty("java.class.path"))
            .append("\" ").append(mainCommand[0]);
      }
      // finally add program arguments
      for (int i = 1; i < mainCommand.length; i++) {
        args.append(' ').append(mainCommand[i]);
      }
      return args.toString();
    }
    return null;
  }

  /**
   * Set the way software should shutdown: with (true) or without restart
   * (false)
   *
   * @param toRestart
   */
  public static void setRestart(final boolean toRestart) {
    shouldRestart = toRestart;
  }

  /**
   * Called to setup main class and args to enable restart
   *
   * @param main
   * @param args
   */
  public static void registerMain(final Class<?> main, final String[] args) {
    if (main == null) {
      applArgs = getArgs();
      return;
    }
    final String path = ManagementFactory.getRuntimeMXBean().getClassPath();
    final StringBuilder newArgs = new StringBuilder();
    if (ParametersChecker.isNotEmpty(path)) {
      newArgs.append("-cp ").append(path);
    }
    newArgs.append(' ').append(main.getName());
    for (final String arg : args) {
      newArgs.append(' ').append(arg);
    }
    applArgs = newArgs.toString();
  }

  @Override
  public void run() {
    if (isShutdownOver) {
      if (shutdownHook != null && shutdownHook.serviceStopped()) {
        try {
          Thread.sleep(WaarpNettyUtil.SIMPLE_DELAY_MS);
        } catch (final InterruptedException e) {//NOSONAR
          SysErrLogger.FAKE_LOGGER.ignoreLog(e);
        }
      }
      // Already stopped
      SysErrLogger.FAKE_LOGGER.syserr(
          "Halt System now - services already stopped -");
      //FBGEXIT DetectionUtils.SystemExit(0)
      return;
    }
    try {
      terminate(false);
    } catch (final Throwable t) {
      if (shutdownHook != null && shutdownHook.serviceStopped()) {
        try {
          Thread.sleep(WaarpNettyUtil.SIMPLE_DELAY_MS);
        } catch (final InterruptedException e) {//NOSONAR
          SysErrLogger.FAKE_LOGGER.ignoreLog(e);
        }
      }
    }
    SysErrLogger.FAKE_LOGGER.syserr("Halt System now");
    //FBGEXIT DetectionUtils.SystemExit(0)
  }

  /**
   * Class for argument of creation of WaarpShutdownHook
   */
  public static class ShutdownConfiguration {
    public long timeout = 30000; // 30s per default
    public WaarpFuture serviceFuture; // no service per default
  }

  /**
   * Finalize resources attached to handlers
   */
  static class ShutdownTimerTask extends TimerTask {
    /**
     * Internal Logger
     */
    private static final WaarpLogger logger =
        WaarpLoggerFactory.getLogger(ShutdownTimerTask.class);

    /**
     * Internal constructor
     */
    ShutdownTimerTask() {
    }

    @Override
    public void run() {
      SysErrLogger.FAKE_LOGGER.syserr("Halt System now - time waiting is over");
      logger.error("System will force EXIT");
      if (shutdownHook != null && shutdownHook.serviceStopped()) {
        try {
          Thread.sleep(WaarpNettyUtil.SIMPLE_DELAY_MS);
        } catch (final InterruptedException e) {//NOSONAR
          SysErrLogger.FAKE_LOGGER.ignoreLog(e);
        }
      }
      if (logger.isDebugEnabled()) {
        final Map<Thread, StackTraceElement[]> map = Thread.getAllStackTraces();
        for (final Entry<Thread, StackTraceElement[]> entry : map.entrySet()) {
          printStackTrace(entry.getKey(), entry.getValue());
        }
      }
      //FBGEXIT DetectionUtils.SystemExit(0)
    }
  }
}