diff --git a/base/src/main/java/de/staropensource/sosengine/base/Engine.java b/base/src/main/java/de/staropensource/sosengine/base/Engine.java
index bb2c8bf..e159e40 100644
--- a/base/src/main/java/de/staropensource/sosengine/base/Engine.java
+++ b/base/src/main/java/de/staropensource/sosengine/base/Engine.java
@@ -32,6 +32,7 @@ import de.staropensource.sosengine.base.exceptions.dependency.UnmetDependenciesE
import de.staropensource.sosengine.base.internal.events.InternalEngineShutdownEvent;
import de.staropensource.sosengine.base.internal.types.DependencySubsystemVector;
import de.staropensource.sosengine.base.logging.CrashHandler;
+import de.staropensource.sosengine.base.logging.InitLogger;
import de.staropensource.sosengine.base.logging.Logger;
import de.staropensource.sosengine.base.logging.LoggerInstance;
import de.staropensource.sosengine.base.types.DependencyVector;
@@ -40,6 +41,7 @@ import de.staropensource.sosengine.base.types.immutable.ImmutableLinkedList;
import de.staropensource.sosengine.base.utility.DependencyResolver;
import de.staropensource.sosengine.base.utility.Miscellaneous;
import de.staropensource.sosengine.base.utility.PlaceholderEngine;
+import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import org.jetbrains.annotations.NotNull;
@@ -107,6 +109,21 @@ public final class Engine extends SubsystemClass {
@Getter
private @NotNull EngineState state = EngineState.UNKNOWN;
+ /**
+ * Contains a list of all registered subsystems.
+ * The list is sorted after initialization order.
+ *
+ * @since v1-alpha1
+ * -- GETTER --
+ * Returns a list of all registered subsystems.
+ * The list is sorted after initialization order.
+ *
+ * @return subsystem list
+ * @since v1-alpha1
+ */
+ @Getter
+ private @NotNull ImmutableLinkedList<@NotNull DependencySubsystemVector> subsystems = new ImmutableLinkedList<>();
+
/**
* Contains the engine's shutdown handler.
* The shutdown handler is responsible for
@@ -136,19 +153,39 @@ public final class Engine extends SubsystemClass {
private @NotNull ShutdownHandler shutdownHandler = new Engine.JvmShutdownHandler();
/**
- * Contains a list of all registered subsystems.
- * The list is sorted after initialization order.
+ * Contains the JVM shutdown hook thread,
+ * which ensures that the engine is fully shut
+ * down before the JVM exits.
*
- * @since v1-alpha1
- * -- GETTER --
- * Returns a list of all registered subsystems.
- * The list is sorted after initialization order.
- *
- * @return subsystem list
- * @since v1-alpha1
+ * @see EngineInternals#installSafetyShutdownHook(boolean)
+ * @since v1-alpha4
*/
- @Getter
- private @NotNull ImmutableLinkedList<@NotNull DependencySubsystemVector> subsystems = new ImmutableLinkedList<>();
+ @Getter(AccessLevel.MODULE)
+ private final @NotNull Thread safetyShutdownHook = Thread.ofPlatform()
+ .name("Engine shutdown thread")
+ .group(getThreadGroup())
+ .unstarted(() -> {
+ // To avoid race condition or something
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException ignored) {}
+
+ // Check if already shutting down
+ switch (state) {
+ case UNKNOWN, SHUTDOWN, CRASHED -> {
+ return;
+ }
+ }
+
+ // Print warning about shutdown
+ logger.warn("Trying to shut down engine using shutdown hook.\nThis approach to shutting down the engine and JVM is NOT RECOMMENDED, please use Engine#shutdown() instead.");
+
+ // Shutdown
+ Engine.getInstance().shutdown();
+
+ // Print last message
+ InitLogger.warn(getClass(), "ENGINE", EngineInformation.getVersioningCodename(), "Engine successfully shut down using shutdown hook. PLEASE USE Engine#shutdown() INSTEAD OF System#exit() or Runtime#exit()!");
+ });
/**
* Initializes the StarOpenSource Engine.
@@ -170,8 +207,9 @@ public final class Engine extends SubsystemClass {
logger.info("Initializing engine");
initializeClasses(); // Initialize classes
- if (checkEnvironment())
+ if (checkEnvironment()) // Check environment
throw new IllegalStateException("Running in an incompatible environment");
+ ensureEnvironment(); // Prepare the environment and ensure safety
populateCrashContent(); // Populate crash content
cacheEvents(); // Cache event listeners
startThreads(); // Start threads
@@ -197,6 +235,19 @@ public final class Engine extends SubsystemClass {
logger.info("Initialized sos!engine %engine_version% (commit %engine_git_commit_id_long%-%engine_git_branch%, dirty %engine_git_dirty%) in " + initTime + "ms");
}
+ /**
+ * Initializes all classes.
+ *
+ * @since v1-alpha0
+ */
+ private void initializeClasses() {
+ logger.verb("Initializing engine classes");
+ new EngineInternals();
+ new PlaceholderEngine();
+
+ EngineInformation.update();
+ }
+
/**
* Checks if the environment is compatible with the engine build.
*
@@ -212,15 +263,12 @@ public final class Engine extends SubsystemClass {
}
/**
- * Initializes all classes.
+ * Ensures the execution safety of the environment.
*
- * @since v1-alpha0
+ * @since v1-alpha4
*/
- private void initializeClasses() {
- logger.verb("Initializing engine classes");
- new PlaceholderEngine();
-
- EngineInformation.update();
+ private void ensureEnvironment() {
+ EngineInternals.getInstance().installSafetyShutdownHook(true);
}
/**
@@ -412,6 +460,12 @@ public final class Engine extends SubsystemClass {
* @since v1-alpha0
*/
public synchronized void shutdown(@Range(from = 0, to = 255) int exitCode) {
+ switch (state) {
+ case UNKNOWN, SHUTDOWN, CRASHED -> {
+ return;
+ }
+ }
+
logger.info("Shutting engine down");
if (state != EngineState.CRASHED)
state = EngineState.SHUTDOWN;
@@ -425,12 +479,19 @@ public final class Engine extends SubsystemClass {
// Flush log messages
Logger.flushLogMessages();
+ // Disable safety shutdown hook
+ try {
+ Runtime.getRuntime().removeShutdownHook(safetyShutdownHook);
+ } catch (Exception ignored) {}
+
+ // Send events
logger.verb("Notifiying classes about shutdown");
new EngineShutdownEvent().callEvent();
logger.verb("Notifying subsystems about shutdown");
new InternalEngineShutdownEvent().callEvent();
+ // Invoke shutdown handler
logger.verb("Invoking shutdown handler (code " + exitCode + ")");
shutdownHandler.shutdown((short) exitCode);
}
@@ -497,8 +558,18 @@ public final class Engine extends SubsystemClass {
/** {@inheritDoc} */
@Override
public void shutdown(short exitCode) {
- logger.warn("Terminating JVM");
+ // Check if already shutting down
+ try {
+ Thread thread = Thread.ofVirtual().unstarted(() -> {});
+ Runtime.getRuntime().addShutdownHook(thread);
+ Runtime.getRuntime().removeShutdownHook(thread);
+ } catch (IllegalStateException exception) {
+ logger.warn("Terminating JVM: Already shutting down, skipping");
+ return;
+ }
+
+ logger.warn("Terminating JVM");
System.exit(exitCode);
}
}
diff --git a/base/src/main/java/de/staropensource/sosengine/base/EngineInternals.java b/base/src/main/java/de/staropensource/sosengine/base/EngineInternals.java
new file mode 100644
index 0000000..552ae80
--- /dev/null
+++ b/base/src/main/java/de/staropensource/sosengine/base/EngineInternals.java
@@ -0,0 +1,117 @@
+/*
+ * STAROPENSOURCE ENGINE SOURCE FILE
+ * Copyright (c) 2024 The StarOpenSource Engine Contributors
+ * Licensed under the GNU Affero General Public License v3
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.staropensource.sosengine.base;
+
+import de.staropensource.sosengine.base.exceptions.NoAccessException;
+import de.staropensource.sosengine.base.logging.LoggerInstance;
+import de.staropensource.sosengine.base.types.InternalAccessArea;
+import lombok.Getter;
+import org.jetbrains.annotations.NotNull;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Class which allows access to the internals
+ * and changing the behaviour of the engine.
+ *
+ * @since v1-alpha4
+ */
+@SuppressWarnings({ "JavadocDeclaration" })
+public final class EngineInternals {
+ /**
+ * Contains the class instance.
+ *
+ * @since v1-alpha4
+ * -- GETTER --
+ * Returns the class instance.
+ *
+ * @return class instance unless the engine is uninitialized
+ * @since v1-alpha4
+ */
+ @Getter
+ private static EngineInternals instance;
+
+ /**
+ * Contains the {@link LoggerInstance} for this instance.
+ *
+ * @see LoggerInstance
+ * @since v1-alpha4
+ */
+ private static final LoggerInstance logger = new LoggerInstance.Builder().setClazz(EngineInternals.class).setOrigin("ENGINE").build();
+
+ /**
+ * Contains all disabled internal access areas.
+ *
+ * @since v1-alpha4
+ */
+ @Getter
+ private final @NotNull List<@NotNull InternalAccessArea> restrictedAreas = new ArrayList<>();
+
+ /**
+ * Constructs this class.
+ *
+ * @since v1-alpha4
+ */
+ public EngineInternals() {
+ // Only allow one instance
+ if (instance == null && Engine.getInstance() != null)
+ instance = this;
+ else
+ logger.crash("Only one instance of this class is allowed, use getInstance() instead of creating a new instance");
+ }
+
+ /**
+ * Allows disabling internal engine access.
+ *
+ * @since v1-alpha4
+ */
+ public void restrictAccess(@NotNull InternalAccessArea area) {
+ if (area == InternalAccessArea.ALL) {
+ List<@NotNull InternalAccessArea> areas = new ArrayList<>(List.of(InternalAccessArea.values()));
+ areas.remove(InternalAccessArea.ALL);
+ restrictedAreas.addAll(areas);
+ }
+
+ restrictedAreas.add(area);
+ }
+
+ /**
+ * Installs or uninstalls the JVM shutdown
+ * hook, which prevents the JVM from exiting
+ * before the engine has fully shut down.
+ * Highly recommended to keep enabled.
+ *
+ * @param status {@code true} to install, {@code false} otherwise
+ * @throws NoAccessException when restricted
+ * @since v1-alpha4
+ */
+ public void installSafetyShutdownHook(boolean status) throws NoAccessException {
+ if (restrictedAreas.contains(InternalAccessArea.SAFETY_SHUTDOWN_HOOK))
+ throw new NoAccessException("The internal access area SAFETY_SHUTDOWN_HOOK has been restricted");
+
+ try {
+ if (status)
+ Runtime.getRuntime().addShutdownHook(Engine.getInstance().getSafetyShutdownHook());
+ else
+ Runtime.getRuntime().removeShutdownHook(Engine.getInstance().getSafetyShutdownHook());
+ } catch (IllegalArgumentException | IllegalStateException ignored) {}
+ }
+}
diff --git a/base/src/main/java/de/staropensource/sosengine/base/logging/Logger.java b/base/src/main/java/de/staropensource/sosengine/base/logging/Logger.java
index 07e67d1..e548ba9 100644
--- a/base/src/main/java/de/staropensource/sosengine/base/logging/Logger.java
+++ b/base/src/main/java/de/staropensource/sosengine/base/logging/Logger.java
@@ -200,6 +200,9 @@ public final class Logger {
if (format == null)
format = EngineConfiguration.getInstance().getLoggerTemplate();
+ // Replace placeholders using PlaceholderEngine
+ format = PlaceholderEngine.getInstance().process(format);
+
// Replace logger placeholders (no colors)
format = new LogClass(issuerClass).replace(format);
format = new de.staropensource.sosengine.base.internal.placeholders.logger.LogLevel(level).replace(format);
@@ -256,7 +259,7 @@ public final class Logger {
format = new LogColorPrimary(level).replace(format);
format = new LogColorSecondary(level).replace(format);
- // Replace placeholders using PlaceholderEngine
+ // Replace placeholders using PlaceholderEngine again
format = PlaceholderEngine.getInstance().process(format);
// Invoke LoggerImpl#postPlaceholder
diff --git a/base/src/main/java/de/staropensource/sosengine/base/types/InternalAccessArea.java b/base/src/main/java/de/staropensource/sosengine/base/types/InternalAccessArea.java
new file mode 100644
index 0000000..56726ba
--- /dev/null
+++ b/base/src/main/java/de/staropensource/sosengine/base/types/InternalAccessArea.java
@@ -0,0 +1,46 @@
+/*
+ * STAROPENSOURCE ENGINE SOURCE FILE
+ * Copyright (c) 2024 The StarOpenSource Engine Contributors
+ * Licensed under the GNU Affero General Public License v3
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero 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 Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.staropensource.sosengine.base.types;
+
+import de.staropensource.sosengine.base.EngineInternals;
+
+/**
+ * Specifies multiple areas of internal engine access.
+ * Used for restricting access of internals.
+ *
+ * @see EngineInternals#restrictAccess(InternalAccessArea)
+ * @since v1-alpha4
+ */
+public enum InternalAccessArea {
+ /**
+ * Refers to all areas.
+ *
+ * @since v1-alpha4
+ */
+ ALL,
+
+ /**
+ * Refers to the toggling of the JVM shutdown hook, which
+ * prevents JVM shutdowns without the engine first shutting down.
+ *
+ * @since v1-alpha4
+ */
+ SAFETY_SHUTDOWN_HOOK,
+}