App.java

package de.dlr.bt.stc;

import java.awt.GraphicsEnvironment;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.util.concurrent.atomic.AtomicBoolean;

import org.greenrobot.eventbus.EventBus;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.Logger;
import de.dlr.bt.stc.cli.CLIOptions;
import de.dlr.bt.stc.init.ModuleInitializer;
import de.dlr.bt.stc.init.STCInstance;
import de.dlr.bt.stc.init.STCInstance.AddConfigurationFolder;
import de.dlr.bt.stc.init.STCInstance.LoadConfiguration;
import de.dlr.bt.stc.init.STCInstance.StartEvent;
import de.dlr.bt.stc.init.STCInstance.StopEvent;
import de.dlr.bt.stc.opcuaserver.UAServer;
import lombok.extern.slf4j.Slf4j;
import picocli.CommandLine;

@Slf4j
public class App {

	private static final int AUTO_RESTART_TIME_MS = 2500;
	private static final int GRACEFUL_TERMINATION_TIMEOUT = 20000;

	private static final AtomicBoolean mainThreadFinished = new AtomicBoolean(false);

	public static void main(String[] args) {

		log.info("Starting ...");

		ModuleInitializer.initialize();

		final var managementEventBus = new EventBus();

		CLIOptions options = new CLIOptions();
		CommandLine cmd = new CommandLine(options);
		cmd.parseArgs(args);

		if (options.isUsageHelpRequested()) {
			cmd.usage(System.out);
			return;
		}

		final boolean isHeadless;
		if (options.isForceHeadless()) {
			isHeadless = true;
		} else {
			isHeadless = GraphicsEnvironment.isHeadless();
		}

		if (options.getLogLevel() != null) {
			Logger rootLogger = (Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME);
			rootLogger.setLevel(options.getLogLevel());
		}

		UAServer uaserver = null;
		if (options.isRunOpcuaServer()) {
			try {
				uaserver = new UAServer(options.getOpcuaServerPort(), managementEventBus);
				uaserver.startup().get();
			} catch (InterruptedException ie) {
				Thread.currentThread().interrupt();
			} catch (Exception ex) {
				log.error("Failed to start OPC/UA server!");
			}
		}

		STCInstance instance = new STCInstance(managementEventBus);

		managementEventBus
				.post(new AddConfigurationFolder(FileSystems.getDefault().getPath(options.getConfigFolder())));
		managementEventBus.post(new LoadConfiguration());
		managementEventBus.post(new StartEvent(AUTO_RESTART_TIME_MS));

		Runtime.getRuntime().addShutdownHook(new Thread(() -> terminateApplication(managementEventBus)));

		log.info("System started, send SIGTERM or SIGINT to stop (Ctrl+C)");

		if (!isHeadless) {
			Thread consoleInput = new Thread(() -> waitForConsoleQuit(instance, managementEventBus),
					"ConsoleStdInThread");
			consoleInput.setDaemon(true);
			consoleInput.start();
		}

		// Hand over main thread to STCInstance, blocking until stop is requested
		instance.run();

		try {
			if (uaserver != null)
				uaserver.shutdown().get();
		} catch (InterruptedException ie) {
			Thread.currentThread().interrupt();
		} catch (Exception ex) {
			log.error("Failed to stop OPC/UA server!");
		}
		log.info("sTC finished");

		mainThreadFinished.set(true);
		synchronized (mainThreadFinished) {
			mainThreadFinished.notifyAll();
		}
	}

	private static void waitForConsoleQuit(STCInstance instance, EventBus managementBus) {
		log.info("Started, press e (followed by enter) to stop");
		while (!instance.isTerminate()) {
			try {
				int chr = System.in.read();
				if (chr == 'e') {
					terminateApplication(managementBus);
				}
			} catch (IOException ioe) {
			}
		}
	}

	/**
	 * Terminates the application. A signal is sent to the management event bus for
	 * graceful termination. If the main thread is still alive after
	 * {@link #GRACEFUL_TERMINATION_TIMEOUT} ms, the JVM will be killed
	 * unconditionally and ungracefully.
	 * 
	 * @param managementBus The management event bus.
	 */
	private static void terminateApplication(EventBus managementBus) {
		// Do nothing after we have already gracefully terminated
		if (mainThreadFinished.get())
			return;

		// Post the stop event
		managementBus.post(new StopEvent(true));

		// Wait for graceful termination
		try {
			var startTime = System.currentTimeMillis();
			while (!mainThreadFinished.get() && System.currentTimeMillis() - startTime < GRACEFUL_TERMINATION_TIMEOUT) {
				synchronized (mainThreadFinished) {
					mainThreadFinished.wait(500);
				}
			}

			// Main thread should have finished by now. If not, terminate the JVM
			// unconditionally

			if (!mainThreadFinished.get()) {
				log.error("The application did not terminate within {} ms. Forcefully terminating now ...",
						GRACEFUL_TERMINATION_TIMEOUT);
				Runtime.getRuntime().halt(-1);
			}
		} catch (InterruptedException ie) {
			log.error("Final wait has been interrupted");
			Thread.currentThread().interrupt();
		}
	}

}