Compare commits

...
Author SHA1 Message Date
Jason Penilla bfaabf0cd0 Implement ANSIComponentSerializer.Provider 2026-02-19 10:43:20 -07:00
Jason Penilla 74d6f984d9 Render completions with correct color level for remote clients 2026-02-15 13:47:41 -07:00
Jason Penilla e70075b661 Remove owen special 2026-02-15 12:56:28 -07:00
Jason Penilla 8023e759a8 Simplify log forwarding setup 2026-02-15 12:53:17 -07:00
Jason Penilla 3a087d6815 Update for serverside forwarded log rendering 2026-02-15 12:44:12 -07:00
Jason Penilla 5b7900e956 update patches after rebase 2026-02-14 21:52:29 -07:00
Jason Penilla 010476a9a0 Disable JBR for now 2026-02-14 21:51:31 -07:00
Jason Penilla e40b59d14d Endermux: configure via system properties and use static instance
- Add paper.endermux.* system properties in PaperEndermux: enabled, maxConnections, and socketPath.
- Rename earlyInstance to INSTANCE and align static config fields to uppercase.
- Start Endermux in bootstrap only when enabled.
- Remove per-server Endermux storage/use and call through PaperEndermux.INSTANCE.
- Clear PaperEndermux.INSTANCE on close.
- Set paper.endermux.enabled=true for run tasks in build.gradle.kts.
2026-02-14 21:51:31 -07:00
Jason Penilla a66b76b26d Update for endermux changes 2026-02-14 21:51:31 -07:00
Jason Penilla 6956141a97 Misc logging fixes 2026-02-14 21:51:31 -07:00
Jason Penilla ed298d0e77 Initialize endermux server earlier 2026-02-14 21:51:31 -07:00
Jason Penilla 1e31be5aad Update for interactivity status support 2026-02-14 21:51:31 -07:00
Jason Penilla 3131ed6b75 Update for Endermux changes 2026-02-14 21:51:30 -07:00
Jason Penilla d8a5b51d31 Use RemoteLogForwarder from endermux-server 2026-02-14 21:51:30 -07:00
Jason Penilla 3c5d51f171 Update RemoteLogForwarder
This class will be moved to shared code eventually
2026-02-14 21:51:30 -07:00
Jason Penilla d65b158662 Initial draft for endermux console protocol support
The idea here is that you could run the server directly from systemd unit, and then attach to the interactive console though a client app over a socket (for now, other connectivity options are possible in the future).
2026-02-14 21:51:25 -07:00
18 changed files with 337 additions and 13 deletions

No files matched your search

+3 -2
View File
@@ -11,7 +11,7 @@ subprojects {
extensions.configure<JavaPluginExtension> {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
languageVersion = JavaLanguageVersion.of(25)
}
}
}
@@ -21,7 +21,7 @@ val paperMavenPublicUrl = "https://repo.papermc.io/repository/maven-public/"
subprojects {
tasks.withType<JavaCompile>().configureEach {
options.encoding = Charsets.UTF_8.name()
options.release = 21
options.release = 25
options.isFork = true
options.compilerArgs.addAll(listOf("-Xlint:-deprecation", "-Xlint:-removal"))
}
@@ -42,6 +42,7 @@ subprojects {
repositories {
mavenCentral()
maven(paperMavenPublicUrl)
mavenLocal()
}
extensions.configure<PublishingExtension> {
+8 -3
View File
@@ -133,6 +133,7 @@ dependencies {
implementation("org.jline:jline-terminal-jni:3.27.1") // fall back to jni on java 21
implementation("net.minecrell:terminalconsoleappender:1.3.0")
implementation("net.kyori:adventure-text-serializer-ansi")
implementation("xyz.jpenilla:endermux-server:0.0.1-SNAPSHOT")
/*
Required to add the missing Log4j2Plugins.dat file from log4j-core
@@ -289,10 +290,14 @@ fun TaskContainer.registerRunTask(
.dir(providers.gradleProperty("paper.runWorkDir").getOrElse("run"))
.asFile
javaLauncher.set(project.javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(21))
vendor.set(JvmVendorSpec.JETBRAINS)
languageVersion.set(JavaLanguageVersion.of(25))
// vendor.set(JvmVendorSpec.JETBRAINS) // JBR 25 is bugged (spark crashes, hotswapping not working)
})
jvmArgs("-XX:+AllowEnhancedClassRedefinition")
jvmArgs(
// "-XX:+AllowEnhancedClassRedefinition", // JBR 25 is bugged (spark crashes, hotswapping not working)
"--enable-native-access=ALL-UNNAMED",
)
systemProperty("paper.endermux.enabled", true)
if (rootProject.childProjects["test-plugin"] != null) {
val testPluginJar = rootProject.project(":test-plugin").tasks.jar.flatMap { it.archiveFile }
@@ -0,0 +1,51 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
Date: Wed, 4 Feb 2026 19:10:35 -0700
Subject: [PATCH] Endermux console protocl support
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index ca2caace6aab0dfcb0ab7a0fd28e66555c62fdc5..397273e17c1b6de17d9b1ed4004a19c9557aa81f 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -1110,6 +1110,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
this.isRestarting = isRestarting;
this.hasLoggedStop = true; // Paper - Debugging
if (isDebugging()) io.papermc.paper.util.TraceUtil.dumpTraceForThread("Server stopped"); // Paper - Debugging
+ if (io.papermc.paper.console.endermux.PaperEndermux.INSTANCE != null) io.papermc.paper.console.endermux.PaperEndermux.INSTANCE.disableInteractivity();
// Paper end
this.running = false;
if (waitForShutdown) {
@@ -1987,7 +1988,10 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@Override
public void sendSystemMessage(Component message) {
- LOGGER.info(io.papermc.paper.adventure.PaperAdventure.ANSI_SERIALIZER.serialize(io.papermc.paper.adventure.PaperAdventure.asAdventure(message))); // Paper - Log message with colors
+ final net.kyori.adventure.text.Component adventureMessage = io.papermc.paper.adventure.PaperAdventure.asAdventure(message);
+ try (final var _ = xyz.jpenilla.endermux.log4j.RichLogContext.pushComponent(adventureMessage)) {
+ LOGGER.info(net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer.ansi().serialize(adventureMessage));
+ }
}
public KeyPair getKeyPair() {
diff --git a/net/minecraft/server/dedicated/DedicatedServer.java b/net/minecraft/server/dedicated/DedicatedServer.java
index eb06d8f012684845146429832e977e6c1ddcd62b..15db6bb08ec3b0ca5b2d417d97b670e80c6f77b8 100644
--- a/net/minecraft/server/dedicated/DedicatedServer.java
+++ b/net/minecraft/server/dedicated/DedicatedServer.java
@@ -270,6 +270,7 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
// Paper end - fix converting txt to json file
org.spigotmc.WatchdogThread.doStart(org.spigotmc.SpigotConfig.timeoutTime, org.spigotmc.SpigotConfig.restartOnCrash); // Paper - start watchdog thread
thread.start(); // Paper - Enhance console tab completions for brigadier commands; start console thread after MinecraftServer.console & PaperConfig are initialized
+ if (io.papermc.paper.console.endermux.PaperEndermux.INSTANCE != null) io.papermc.paper.console.endermux.PaperEndermux.INSTANCE.enableInteractivity(this);
io.papermc.paper.command.PaperCommands.registerCommands(this); // Paper - setup /paper command
this.server.spark.registerCommandBeforePlugins(this.server); // Paper - spark
com.destroystokyo.paper.Metrics.PaperMetrics.startMetrics(); // Paper - start metrics
@@ -540,6 +541,8 @@ public class DedicatedServer extends MinecraftServer implements ServerInterface
}
}
+ if (io.papermc.paper.console.endermux.PaperEndermux.INSTANCE != null) io.papermc.paper.console.endermux.PaperEndermux.INSTANCE.close();
+
this.hasFullyShutdown = true; // Paper - Improved watchdog support
System.exit(this.abnormalExit ? 70 : 0); // CraftBukkit // Paper - Improved watchdog support
}
@@ -1,6 +1,7 @@
package io.papermc.paper;
import java.util.List;
import io.papermc.paper.console.endermux.PaperEndermux;
import joptsimple.OptionSet;
import net.minecraft.SharedConstants;
import net.minecraft.server.Main;
@@ -16,6 +17,11 @@ public final class PaperBootstrap {
public static void boot(final OptionSet options) {
SharedConstants.tryDetectVersion();
if (PaperEndermux.ENABLED) {
PaperEndermux.INSTANCE = new PaperEndermux();
PaperEndermux.INSTANCE.start();
}
getStartupVersionMessages().forEach(LOGGER::info);
Main.main(options);
@@ -29,7 +29,6 @@ import net.kyori.adventure.text.flattener.ComponentFlattener;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.serializer.ComponentSerializer;
import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainComponentSerializer;
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
@@ -121,7 +120,6 @@ public final class PaperAdventure {
public static final AttributeKey<Locale> LOCALE_ATTRIBUTE = AttributeKey.valueOf("adventure:locale"); // init after FLATTENER because classloading triggered here might create a logger
@Deprecated
public static final PlainComponentSerializer PLAIN = PlainComponentSerializer.builder().flattener(FLATTENER).build();
public static final ANSIComponentSerializer ANSI_SERIALIZER = ANSIComponentSerializer.builder().flattener(FLATTENER).build();
private static final TagParser<Tag> NBT_PARSER = TagParser.create(NbtOps.INSTANCE);
public static final Codec<Tag, String, CommandSyntaxException, RuntimeException> NBT_CODEC = new Codec<>() {
@Override
@@ -0,0 +1,20 @@
package io.papermc.paper.adventure.providers;
import io.papermc.paper.adventure.PaperAdventure;
import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer;
import org.jspecify.annotations.NullMarked;
import java.util.function.Consumer;
@NullMarked
@SuppressWarnings("UnstableApiUsage") // permitted provider
public class ANSIComponentSerializerProviderImpl implements ANSIComponentSerializer.Provider {
@Override
public ANSIComponentSerializer ansi() {
return ANSIComponentSerializer.builder().flattener(PaperAdventure.FLATTENER).build();
}
@Override
public Consumer<ANSIComponentSerializer.Builder> builder() {
return builder -> builder.flattener(PaperAdventure.FLATTENER);
}
}
@@ -1,10 +1,10 @@
package io.papermc.paper.adventure.providers;
import io.papermc.paper.adventure.PaperAdventure;
import java.util.Locale;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.logger.slf4j.ComponentLogger;
import net.kyori.adventure.text.logger.slf4j.ComponentLoggerProvider;
import net.kyori.adventure.text.serializer.ansi.ANSIComponentSerializer;
import net.kyori.adventure.translation.GlobalTranslator;
import org.jetbrains.annotations.NotNull;
import org.slf4j.LoggerFactory;
@@ -17,6 +17,6 @@ public class ComponentLoggerProviderImpl implements ComponentLoggerProvider {
}
private String serialize(final Component message) {
return PaperAdventure.ANSI_SERIALIZER.serialize(GlobalTranslator.render(message, Locale.getDefault()));
return ANSIComponentSerializer.ansi().serialize(GlobalTranslator.render(message, Locale.getDefault()));
}
}
@@ -22,6 +22,7 @@ import org.checkerframework.checker.nullness.qual.Nullable;
import org.jline.reader.Candidate;
import org.jline.reader.LineReader;
import org.jline.reader.ParsedLine;
import xyz.jpenilla.endermux.ansi.ColorLevelContext;
import static com.destroystokyo.paper.event.server.AsyncTabCompleteEvent.Completion.completion;
@@ -86,7 +87,7 @@ public final class BrigadierCommandCompleter {
}
private static @NonNull Candidate toCandidate(final @NonNull String suggestionText, final @Nullable Component tooltip) {
final String suggestionTooltip = PaperAdventure.ANSI_SERIALIZER.serializeOr(tooltip, null);
final String suggestionTooltip = ColorLevelContext.currentSerializer().serializeOr(tooltip, null);
//noinspection SpellCheckingInspection
return new PaperCandidate(
suggestionText,
@@ -8,6 +8,7 @@ import com.mojang.brigadier.tree.LiteralCommandNode;
import java.util.function.Supplier;
import java.util.regex.Pattern;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.dedicated.DedicatedServer;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.jline.reader.Highlighter;
@@ -28,12 +29,20 @@ public final class BrigadierCommandHighlighter implements Highlighter {
@Override
public AttributedString highlight(final @NonNull LineReader reader, final @NonNull String buffer) {
return highlight(this.server, this.commandSourceStack, buffer);
}
public static AttributedString highlight(
final MinecraftServer server,
final Supplier<CommandSourceStack> stack,
final String buffer
) {
//noinspection ConstantConditions
if (this.server.overworld() == null) { // check if overworld is null, as worlds haven't been loaded yet
if (server.overworld() == null) { // check if overworld is null, as worlds haven't been loaded yet
return new AttributedString(buffer, AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
}
final AttributedStringBuilder builder = new AttributedStringBuilder();
final ParseResults<CommandSourceStack> results = this.server.getCommands().getDispatcher().parse(new StringReader(buffer), this.commandSourceStack.get());
final ParseResults<CommandSourceStack> results = server.getCommands().getDispatcher().parse(new StringReader(buffer), stack.get());
int pos = 0;
int component = -1;
for (final ParsedCommandNode<CommandSourceStack> node : results.getContext().getLastChild().getNodes()) {
@@ -0,0 +1,53 @@
package io.papermc.paper.console.endermux;
import io.papermc.paper.console.BrigadierConsoleParser;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.ArrayList;
import java.util.List;
import net.minecraft.server.dedicated.DedicatedServer;
import org.bukkit.craftbukkit.command.ConsoleCommandCompleter;
import org.jline.reader.Candidate;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.ParsedLine;
import org.jline.terminal.TerminalBuilder;
import org.jspecify.annotations.NullMarked;
import xyz.jpenilla.endermux.protocol.Payloads;
import xyz.jpenilla.endermux.server.api.InteractiveConsoleHooks;
@NullMarked
public final class PaperCommandCompleter implements InteractiveConsoleHooks.CommandCompleter {
private final DedicatedServer server;
private final LineReader lineReader;
public PaperCommandCompleter(final DedicatedServer server) {
this.server = server;
try {
this.lineReader = LineReaderBuilder.builder()
.terminal(TerminalBuilder.builder().dumb(true).build())
.build();
} catch (final IOException e) {
throw new UncheckedIOException(e);
}
}
@Override
public Payloads.CompletionResponse complete(final String command, final int cursor) {
final ParsedLine parsedLine = new BrigadierConsoleParser(this.server).parse(command, cursor);
final List<Candidate> candidates = new ArrayList<>();
final ConsoleCommandCompleter completer = new ConsoleCommandCompleter(this.server);
completer.complete(this.lineReader, parsedLine, candidates);
final List<Payloads.CompletionResponse.CandidateInfo> candidateInfos = new ArrayList<>();
for (final Candidate candidate : candidates) {
candidateInfos.add(new Payloads.CompletionResponse.CandidateInfo(
candidate.value(),
candidate.displ(),
candidate.descr()
));
}
return new Payloads.CompletionResponse(candidateInfos);
}
}
@@ -0,0 +1,19 @@
package io.papermc.paper.console.endermux;
import net.minecraft.server.dedicated.DedicatedServer;
import org.jspecify.annotations.NullMarked;
import xyz.jpenilla.endermux.server.api.InteractiveConsoleHooks;
@NullMarked
public final class PaperCommandExecutor implements InteractiveConsoleHooks.CommandExecutor {
private final DedicatedServer server;
public PaperCommandExecutor(final DedicatedServer server) {
this.server = server;
}
@Override
public void execute(final String command) {
this.server.handleConsoleInput(command, this.server.createCommandSourceStack());
}
}
@@ -0,0 +1,34 @@
package io.papermc.paper.console.endermux;
import io.papermc.paper.console.BrigadierCommandHighlighter;
import net.minecraft.server.dedicated.DedicatedServer;
import org.jline.utils.AttributedStringBuilder;
import org.jline.utils.AttributedStyle;
import org.jspecify.annotations.NullMarked;
import xyz.jpenilla.endermux.protocol.Payloads;
import xyz.jpenilla.endermux.server.api.InteractiveConsoleHooks;
@NullMarked
public final class PaperCommandHighlighter implements InteractiveConsoleHooks.CommandHighlighter {
private final DedicatedServer server;
public PaperCommandHighlighter(final DedicatedServer server) {
this.server = server;
}
@Override
public Payloads.SyntaxHighlightResponse highlight(final String command) {
return new Payloads.SyntaxHighlightResponse(command, this.highlightCommand(command));
}
private String highlightCommand(final String command) {
try {
return BrigadierCommandHighlighter.highlight(this.server, this.server::createCommandSourceStack, command).toAnsi();
} catch (final Exception e) {
final AttributedStringBuilder builder = new AttributedStringBuilder();
builder.style(AttributedStyle.DEFAULT.foreground(AttributedStyle.RED));
builder.append(command);
return builder.toAnsi();
}
}
}
@@ -0,0 +1,31 @@
package io.papermc.paper.console.endermux;
import io.papermc.paper.console.BrigadierConsoleParser;
import net.minecraft.server.dedicated.DedicatedServer;
import org.jline.reader.ParsedLine;
import org.jspecify.annotations.NullMarked;
import xyz.jpenilla.endermux.protocol.Payloads;
import xyz.jpenilla.endermux.server.api.InteractiveConsoleHooks;
@NullMarked
public final class PaperCommandParser implements InteractiveConsoleHooks.CommandParser {
private final DedicatedServer server;
public PaperCommandParser(final DedicatedServer server) {
this.server = server;
}
@Override
public Payloads.ParseResponse parse(final String command, final int cursor) {
final ParsedLine parsedLine = new BrigadierConsoleParser(this.server).parse(command, cursor);
return new Payloads.ParseResponse(
parsedLine.word(),
parsedLine.wordCursor(),
parsedLine.wordIndex(),
parsedLine.words(),
parsedLine.line(),
parsedLine.cursor()
);
}
}
@@ -0,0 +1,78 @@
package io.papermc.paper.console.endermux;
import java.nio.file.Path;
import java.nio.file.Paths;
import net.minecraft.server.dedicated.DedicatedServer;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import xyz.jpenilla.endermux.server.EndermuxServer;
import xyz.jpenilla.endermux.server.api.InteractiveConsoleHooks;
import xyz.jpenilla.endermux.server.log4j.EndermuxForwardingAppender;
@NullMarked
public final class PaperEndermux {
private static final String PROPERTY_PREFIX = "paper.endermux.";
private static final boolean DEFAULT_ENABLED = false;
private static final int DEFAULT_MAX_CONNECTIONS = 5;
private static final String DEFAULT_SOCKET_PATH = "console.sock";
public static final boolean ENABLED = Boolean.parseBoolean(System.getProperty(PROPERTY_PREFIX + "enabled", Boolean.toString(DEFAULT_ENABLED)));
public static final int MAX_CONNECTIONS = readInt(PROPERTY_PREFIX + "maxConnections", DEFAULT_MAX_CONNECTIONS);
public static final Path SOCKET_PATH = Paths.get(System.getProperty(PROPERTY_PREFIX + "socketPath", DEFAULT_SOCKET_PATH));
public static @Nullable PaperEndermux INSTANCE;
private @Nullable EndermuxServer endermuxServer;
private static int readInt(final String key, final int defaultValue) {
final String raw = System.getProperty(key);
if (raw == null) {
return defaultValue;
}
try {
return Integer.parseInt(raw);
} catch (final NumberFormatException ignored) {
return defaultValue;
}
}
public void start() {
this.endermuxServer = new EndermuxServer(
SOCKET_PATH,
MAX_CONNECTIONS
);
EndermuxForwardingAppender.attach(this.endermuxServer);
this.endermuxServer.start();
}
public void enableInteractivity(final DedicatedServer server) {
if (this.endermuxServer != null) {
this.endermuxServer.enableInteractivity(
InteractiveConsoleHooks.builder()
.completer(new PaperCommandCompleter(server))
.parser(new PaperCommandParser(server))
.executor(new PaperCommandExecutor(server))
.highlighter(new PaperCommandHighlighter(server))
.build()
);
}
}
public void disableInteractivity() {
if (this.endermuxServer != null) {
this.endermuxServer.disableInteractivity();
}
}
public void close() {
if (this.endermuxServer != null) {
this.endermuxServer.stop();
this.endermuxServer = null;
}
EndermuxForwardingAppender.detach();
INSTANCE = null;
}
}
@@ -22,6 +22,11 @@ public final class LogManagerShutdownThread extends Thread {
@Override
public void run() {
LoggerShutdown.shutdownLogging();
io.papermc.paper.log.CustomLogManager.forceReset(); // Paper - Reset loggers after shutdown
LoggerShutdown.shutdownLogging(); // Flushes the async appender
try {
net.minecrell.terminalconsole.TerminalConsoleAppender.close(); // Paper - Use TerminalConsoleAppender
} catch (final Exception ignored) {
}
}
}
@@ -0,0 +1 @@
io.papermc.paper.adventure.providers.ANSIComponentSerializerProviderImpl
@@ -14,6 +14,16 @@
</LoggerNamePatternSelector>
</PatternLayout>
</TerminalConsole>
<EndermuxForwardingAppender name="Endermux">
<PatternLayout>
<EndermuxLoggerNamePatternSelector defaultPattern="%EndermuxHighlightError{[%d{HH:mm:ss} %level]: [%logger] %EndermuxRichMessage{%msg}%n%xEx{full}}">
<!-- Log root, Minecraft, Mojang and Bukkit loggers without prefix -->
<!-- Disable prefix for various plugins that bypass the plugin logger -->
<PatternMatch key=",net.minecraft.,Minecraft,com.mojang.,com.sk89q.,ru.tehkode.,Minecraft.AWE"
pattern="%EndermuxHighlightError{[%d{HH:mm:ss} %level]: %EndermuxRichMessage{%msg}%n%xEx{full}}" />
</EndermuxLoggerNamePatternSelector>
</PatternLayout>
</EndermuxForwardingAppender>
<RollingRandomAccessFile name="File" fileName="logs/latest.log" filePattern="logs/%d{yyyy-MM-dd}-%i.log.gz">
<PatternLayout>
<LoggerNamePatternSelector defaultPattern="[%d{HH:mm:ss}] [%t/%level]: [%logger] %stripAnsi{%msg}%n%xEx{full}">
@@ -33,6 +43,7 @@
<AppenderRef ref="File"/>
<AppenderRef ref="TerminalConsole" level="info"/>
<AppenderRef ref="ServerGuiConsole" level="info"/>
<AppenderRef ref="Endermux" level="info"/>
</Async>
</Appenders>
<Loggers>
@@ -1,5 +1,6 @@
package io.papermc.testplugin;
import com.destroystokyo.paper.exception.ServerInternalException;
import org.bukkit.event.Listener;
import org.bukkit.plugin.java.JavaPlugin;