Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions chunky/src/java/se/llbit/chunky/Plugin.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package se.llbit.chunky;

import se.llbit.chunky.main.Chunky;
import se.llbit.util.concurrent.ChunkyThread;

/**
* The plugin interface for Chunky plugins.
Expand All @@ -33,4 +34,17 @@ public interface Plugin {
* @param chunky Chunky instance which the plugin should attach to
*/
void attach(Chunky chunky);

/**
* Called when chunky shuts down, allowing the plugin to close critical resources. Most plugins do not need to do
* anything here.
*
* <p>This function will be called after all {@link ChunkyThread}s have shut down. Chunky's UI thread may still
* be running.</p>
* <p>This method will not be called if any {@link ChunkyThread} does not shut down within its given time limit</p>
* <p>This method may not be called if chunky terminates in a non-normal way, such as through {@link Runtime#halt},
* the user killing the process (<code>SIGKILL</code> & <code>TerminateProcess</code>), or an error within a native
* function.</p>
*/
default void shutdown(Chunky chunky) { }
}
75 changes: 30 additions & 45 deletions chunky/src/java/se/llbit/chunky/main/Chunky.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import se.llbit.chunky.plugin.*;
import se.llbit.chunky.plugin.loader.PluginManager;
import se.llbit.chunky.plugin.loader.JarPluginLoader;
import se.llbit.chunky.plugin.manifest.PluginManifest;
import se.llbit.chunky.renderer.*;
import se.llbit.chunky.renderer.RenderManager;
import se.llbit.chunky.renderer.export.PictureExportFormat;
Expand All @@ -39,24 +38,22 @@
import se.llbit.chunky.resources.ResourcePackLoader;
import se.llbit.chunky.resources.SettingsDirectory;
import se.llbit.chunky.ui.ChunkyFx;
import se.llbit.chunky.ui.controller.CreditsController;
import se.llbit.chunky.ui.render.RenderControlsTabTransformer;
import se.llbit.chunky.world.MaterialStore;
import se.llbit.json.JsonArray;
import se.llbit.log.ConsoleReceiver;
import se.llbit.log.Level;
import se.llbit.log.Log;
import se.llbit.log.Receiver;
import se.llbit.util.TaskTracker;
import se.llbit.util.concurrent.ChunkyThread;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ForkJoinPool;
import java.util.stream.Collectors;
import java.util.concurrent.TimeUnit;

/**
* Chunky is a Minecraft mapping and rendering tool created byJesper Öqvist (jesper@llbit.se).
Expand Down Expand Up @@ -85,6 +82,7 @@ public void logEvent(Level level, String message) {
};

public final ChunkyOptions options;
private final PluginManager pluginManager;
private RenderController renderController;
private SceneFactory sceneFactory = SceneFactory.DEFAULT;
private RenderContextFactory renderContextFactory = RenderContext::new;
Expand All @@ -106,6 +104,7 @@ public static String getMainWindowTitle() {

public Chunky(ChunkyOptions options) {
this.options = options;
this.pluginManager = new PluginManager(BuiltInPlugins.PLUGINS, new JarPluginLoader());
registerBlockProvider(new MinecraftBlockProvider());
registerBlockProvider(new LegacyMinecraftBlockProvider());
}
Expand Down Expand Up @@ -211,9 +210,8 @@ public static void main(final String[] args) {
System.exit(1);
}

int exitCode = 0;
if (cmdline.mode == CommandLineOptions.Mode.CLI_OPERATION) {
exitCode = cmdline.exitCode;
System.exit(cmdline.exitCode);
} else {
// Initialize the common thread pool.
getCommonThreads();
Expand All @@ -222,6 +220,14 @@ public static void main(final String[] args) {
chunky.headless = cmdline.mode == Mode.HEADLESS_RENDER || cmdline.mode == Mode.CREATE_SNAPSHOT;
chunky.loadPlugins();

Runtime.getRuntime().addShutdownHook(
new Thread(() -> { // intentionally not a ChunkyThread
// Within a shutdown hook we need to close quickly, otherwise we risk the user/OS escalating to KILL
chunky.shutdown(1, TimeUnit.SECONDS);
})
);

int exitCode = 0;
try {
switch (cmdline.mode) {
case HEADLESS_RENDER:
Expand All @@ -240,12 +246,21 @@ public static void main(final String[] args) {
Log.error("Unchecked exception caused Chunky to close.", t);
exitCode = 2;
}
}
if (exitCode != 0) {
chunky.shutdown(5, TimeUnit.SECONDS);
// Always exit, we've done all shutdown necessary and want to exit whether non-daemon threads exist or not.
// This should prevent hangs if threads aren't cooperating.
System.exit(exitCode);
}
}

private void shutdown(int timeout, TimeUnit unit) {
if (ChunkyThread.interruptAndJoinAll(timeout, unit)) {
pluginManager.shutdownPlugins(plugin -> plugin.shutdown(this));
} else {
Log.error("Not all threads were joined before shutting down."); // FIXME: list all alive threads? ThreadGroups are annoying.
}
}

/**
* This can be used by plugins to load the default Minecraft textures.
*/
Expand All @@ -255,40 +270,10 @@ public static void loadDefaultTextures() {
}

private void loadPlugins() {
File pluginsDirectory = SettingsDirectory.getPluginsDirectory();
if (!pluginsDirectory.isDirectory()) {
Log.infof("Plugins directory does not exist: %s", pluginsDirectory.getAbsolutePath());
return;
}
Path pluginsPath = pluginsDirectory.toPath();
JsonArray plugins = PersistentSettings.getPlugins();
// TODO: allow plugins to implement a custom plugin loader.
PluginManager pluginManager = new PluginManager(new JarPluginLoader());

// Parse plugin manifests
Set<PluginManifest> pluginManifests = plugins.elements.stream()
.map(value -> value.asString(""))
.filter(jarName -> !jarName.isEmpty())
.map(jarName -> pluginsPath.resolve(jarName).toAbsolutePath().toFile())
.map(PluginManager::parsePluginManifest)
.flatMap(Optional::stream)
.collect(Collectors.toSet());

// Load plugins
pluginManager.load(pluginManifests, (plugin, manifest) -> {
String jarName = manifest.pluginJar.getName();
Log.infof("Loading plugin: %s", jarName);
if (!isHeadless()) {
CreditsController.addPlugin(manifest.name, manifest.version.toString(), manifest.author, manifest.description);
}

try {
plugin.attach(this);
} catch (Throwable t) {
Log.error("Plugin " + jarName + " failed to load.", t);
}
Log.infof("Plugin loaded: %s %s", manifest.name, manifest.version);
});
this.pluginManager.loadPluginsFromDirectory(
SettingsDirectory.getPluginsDirectory(),
(plugin, manifest) -> plugin.attach(this)
);
}

/**
Expand Down Expand Up @@ -342,7 +327,7 @@ public void update() {
public static ForkJoinPool getCommonThreads() {
if (commonThreads == null) {
// use at least two threads to prevent deadlocks in some java versions (see #1631)
commonThreads = new ForkJoinPool(Math.max(PersistentSettings.getNumThreads(), 2));
commonThreads = ChunkyThread.addForkJoinPool(new ForkJoinPool(Math.max(PersistentSettings.getNumThreads(), 2)));
}
return commonThreads;
}
Expand All @@ -353,7 +338,7 @@ public static ForkJoinPool getCommonThreads() {
*/
public static void setCommonThreadsCount(int threads) {
ForkJoinPool t = getCommonThreads();
commonThreads = new ForkJoinPool(threads);
commonThreads = ChunkyThread.addForkJoinPool(new ForkJoinPool(threads));
t.shutdown();
}

Expand Down
31 changes: 31 additions & 0 deletions chunky/src/java/se/llbit/chunky/plugin/BuiltInPlugins.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package se.llbit.chunky.plugin;

import org.apache.maven.artifact.versioning.ArtifactVersion;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import org.apache.maven.artifact.versioning.VersionRange;
import se.llbit.chunky.main.Version;
import se.llbit.chunky.plugin.manifest.PluginManifest;

import java.util.Collection;
import java.util.List;

public class BuiltInPlugins {
/**
* The plugins built into chunky
*/
public static final Collection<PluginManifest> PLUGINS;

/**
* Built-in plugins are the same version as chunky
*/
private static final ArtifactVersion BUILT_IN_PLUGINS_VERSION = new DefaultArtifactVersion(Version.getVersion());
/**
* Built-in plugins target the current chunky version
*/
private static final VersionRange BUILT_IN_PLUGINS_VERSION_RANGE = VersionRange.createFromVersion(Version.getVersion());

static {
PLUGINS = List.of(
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@
public class JarPluginLoader implements PluginLoader {
public void load(BiConsumer<Plugin, PluginManifest> onLoad, PluginManifest pluginManifest) {
try {
Class<?> pluginClass = loadPluginClass(pluginManifest.main, pluginManifest.pluginJar);
Class<?> pluginClass;
if (pluginManifest.pluginJar.isPresent()) {
File pluginJar = pluginManifest.pluginJar.get();
pluginClass = loadPluginClass(pluginManifest.mainClass, pluginJar);
} else {
// No plugin jar is present, class must already be loaded.
pluginClass = Class.forName(pluginManifest.mainClass);
}
Plugin plugin = (Plugin) pluginClass.getDeclaredConstructor().newInstance();
onLoad.accept(plugin, pluginManifest);
} catch (IOException | ClassNotFoundException e) {
Expand Down
Loading
Loading