Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ The allow-listed kinds are `overview`, `vote-site-health`, `player`, `vote-log-s
Maintain these global bounds unless a versioned contract deliberately replaces them:

- result JSON: 512 KiB;
- general result rows: 100 (diagnostics may report up to 128 detected plugin names);
- general result rows: 100 (including detected plugin names in diagnostics);
- top lists: 20;
- lookback: 365 days;
- exact player lookup only;
Expand Down
21 changes: 20 additions & 1 deletion VotingPlugin/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
Expand All @@ -89,6 +94,10 @@
<groupId>com.velocitypowered</groupId>
<artifactId>velocity-brigadier</artifactId>
</exclusion>
<exclusion>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
</exclusion>
</exclusions>
</path>
</annotationProcessorPaths>
Expand Down Expand Up @@ -164,6 +173,10 @@
<pattern>io.leangen.geantyref</pattern>
<shadedPattern>${project.groupId}.simpleapi.geantyref</shadedPattern>
</relocation>
<relocation>
<pattern>org.yaml.snakeyaml</pattern>
<shadedPattern>${project.groupId}.votingplugin.snakeyaml</shadedPattern>
</relocation>
</relocations>
</configuration>
<executions>
Expand Down Expand Up @@ -254,6 +267,12 @@
</repository>
</repositories>
<dependencies>
<!-- Must precede Spigot, which embeds an older SnakeYAML API on its provided compile path. -->
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>org.spigotmc</groupId>
<artifactId>spigot-api</artifactId>
Expand Down Expand Up @@ -692,4 +711,4 @@
</build>
</profile>
</profiles>
</project>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -1244,6 +1244,14 @@ public synchronized void restartBackendProxyHandler() {
}
}

/** Applies only proxy communication settings without reloading unrelated Bukkit configuration. */
public synchronized void reloadBackendProxyMethodFromControl() {
bungeeSettings.reloadData();
getOptions().setServer(bungeeSettings.getServer());
updateAdvancedCoreHook();
restartBackendProxyHandler();
}

/** Keeps one plugin-message listener for the plugin lifetime and atomically swaps its active backend handler. */
public synchronized void activateBackendPluginMessageHandler(GlobalMessageHandler target) {
backendPluginMessageTarget.set(target);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@ public ApplyResult apply(String fileName, String proposedContent, String expecte

private ApplyResult apply(String fileName, String proposedContent, String expectedRevision,
boolean restoreRedactedSecrets) throws IOException {
return apply(fileName, proposedContent, expectedRevision, restoreRedactedSecrets, reload);
}

private ApplyResult apply(String fileName, String proposedContent, String expectedRevision,
boolean restoreRedactedSecrets, ApplyAction applyAction) throws IOException {
Path target = resolve(fileName);
String current = readRaw(target, false);
if (expectedRevision == null || !revision(current).equals(expectedRevision)) throw new StaleRevisionException();
Expand All @@ -120,11 +125,11 @@ private ApplyResult apply(String fileName, String proposedContent, String expect
installed = true;
throw published;
}
reload.run(fileName);
applyAction.run(fileName);
String applied = readRaw(target, false);
String installedRevision = revision(preview.resolvedContent());
if (!revision(applied).equals(installedRevision)) {
reconcileConcurrentEdit(fileName, target);
reconcileConcurrentEdit(fileName, target, applyAction);
throw new StaleRevisionException();
}
return new ApplyResult(new Document(fileName, mask(parse(applied)), revision(applied)),
Expand All @@ -146,7 +151,7 @@ private ApplyResult apply(String fileName, String proposedContent, String expect
// still reload it so runtime and disk cannot diverge.
failure.addSuppressed(published);
}
reload.run(fileName);
applyAction.run(fileName);
rolledBack = true;
} catch (Exception rollbackFailure) {
failure.addSuppressed(rollbackFailure);
Expand All @@ -159,10 +164,10 @@ private ApplyResult apply(String fileName, String proposedContent, String expect
}
}

private void reconcileConcurrentEdit(String fileName, Path target) throws Exception {
private void reconcileConcurrentEdit(String fileName, Path target, ApplyAction applyAction) throws Exception {
for (int attempt = 0; attempt < 3; attempt++) {
String snapshotRevision = revision(readRaw(target, false));
reload.run(fileName);
applyAction.run(fileName);
if (revision(readRaw(target, false)).equals(snapshotRevision)) return;
}
throw new StaleRevisionException();
Expand Down Expand Up @@ -330,6 +335,11 @@ String currentQuickSetupRevision(String preset, Map<String, String> options) thr

public ApplyResult applyQuickSetup(String preset, Map<String, String> options, String expectedRevision)
throws IOException {
return applyQuickSetup(preset, options, expectedRevision, reload);
}

ApplyResult applyQuickSetup(String preset, Map<String, String> options, String expectedRevision,
ApplyAction applyAction) throws IOException {
String fileName = quickSetupFile(preset, options);
String current = readRaw(resolve(fileName), false);
if (expectedRevision == null || !quickSetupRevision(preset, current).equals(expectedRevision)) {
Expand All @@ -338,7 +348,7 @@ public ApplyResult applyQuickSetup(String preset, Map<String, String> options, S
QuickProposal proposal = quickProposal(preset, options, fileName, current);
// Quick proposals are generated from this fresh, unmasked server snapshot.
// Editor placeholder restoration must remain limited to client-authored YAML.
ApplyResult applied = apply(proposal.fileName(), proposal.content(), revision(current), false);
ApplyResult applied = apply(proposal.fileName(), proposal.content(), revision(current), false, applyAction);
if (!"sync-vote-sites".equals(preset)) return applied;
Document document = applied.document();
String installed = readRaw(resolve(fileName), false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,14 +111,21 @@ private BackendControlConnector(VotingPluginMain plugin, Path dataDirectory, Set
}

private void reloadConfiguration(String fileName) throws Exception {
reloadOnServerThread(() -> {
plugin.reloadFromControl();
if ("BungeeSettings.yml".equals(fileName)) plugin.restartBackendProxyHandler();
});
}

private void reloadProxyMethod(String ignored) throws Exception {
reloadOnServerThread(plugin::reloadBackendProxyMethodFromControl);
}

private void reloadOnServerThread(Runnable action) throws Exception {
Future<?> reload;
synchronized (operationLifecycle) {
if (closed) throw new IllegalStateException("Bukkit Control connector is stopping");
reload = plugin.getServer().getScheduler().callSyncMethod(plugin, () -> {
plugin.reloadFromControl();
if ("BungeeSettings.yml".equals(fileName)) plugin.restartBackendProxyHandler();
return null;
});
reload = plugin.getServer().getScheduler().callSyncMethod(plugin, () -> { action.run(); return null; });
activeReload = reload;
}
try {
Expand Down Expand Up @@ -504,10 +511,20 @@ public void publishHostedConfiguration(HostConfiguration configuration, Runnable
private void persistIntent(UUID operationId, TaskResult anticipated, String attemptId) throws IOException {
JsonObject result = anticipated.json();
result.addProperty("attemptId", attemptId);
StoredResult previous;
synchronized (completed) {
completed.put(operationId, new StoredResult(result, anticipated.restartConnector(), false, false));
previous = completed.put(operationId,
new StoredResult(result, anticipated.restartConnector(), false, false));
}
try {
persistCompleted();
} catch (IOException failure) {
synchronized (completed) {
if (previous == null) completed.remove(operationId);
else completed.put(operationId, previous);
}
throw failure;
}
persistCompleted();
}

private void prepareWriteAheadIntents() throws IOException {
Expand All @@ -527,7 +544,17 @@ private void prepareWriteAheadIntents() throws IOException {
}
}
}
if (changed) persistCompleted();
if (changed) {
try {
persistCompleted();
} catch (IOException failure) {
synchronized (completed) {
completed.clear();
completed.putAll(snapshot);
}
throw failure;
}
}
}

static StoredResult abortedIntent(StoredResult pending) {
Expand Down Expand Up @@ -633,8 +660,8 @@ private TaskResult executeFile(UUID operationId, String type, JsonObject configu
if ("APPLY".equals(type)) {
BackendConfigurationService.Preview preview = configurations.preview(fileName, content);
persistIntent(operationId,
TaskResult.file(configurations.proposedDocument(preview), preview.changes(), true, false,
"Config.yml".equals(fileName)), string(task, "attemptId"));
TaskResult.fileIntent(fileName, configurations.proposedDocument(preview).revision(),
preview.changes(), "Config.yml".equals(fileName)), string(task, "attemptId"));
BackendConfigurationService.ApplyResult applied = configurations.apply(fileName, content,
string(task, "expectedRevision"));
return TaskResult.file(applied.document(), applied.changes(), true, applied.rolledBack(),
Expand Down Expand Up @@ -664,8 +691,10 @@ private TaskResult executeQuick(UUID operationId, String type, JsonObject config
TaskResult.quick(preset, options, configurations.proposedQuickSetupRevision(preset, preview), preview.changes(),
true, "Config.yml".equals(preview.proposal().fileName())), string(task, "attemptId"));
BackendConfigurationService.ApplyResult applied = configurations.applyQuickSetup(preset, options,
string(task, "expectedRevision"));
return TaskResult.quick(preset, options, applied.document().revision(), applied.changes(), true,
string(task, "expectedRevision"), "proxy-method".equals(preset)
? this::reloadProxyMethod : this::reloadConfiguration);
return TaskResult.quick(preset, options, applied.document().revision(), applied.changes(),
!"proxy-method".equals(preset),
"Config.yml".equals(applied.document().fileName()));
}
return TaskResult.failure("UNSUPPORTED_TASK", "Task type is unsupported");
Expand Down Expand Up @@ -902,10 +931,20 @@ private static TaskResult file(BackendConfigurationService.Document document, Li
List.copyOf(changes), reloaded, rolledBack, restartConnector);
}

private static TaskResult fileIntent(String fileName, String revision, List<String> changes,
boolean restartConnector) {
JsonObject config = new JsonObject();
config.addProperty("domain", "file");
config.addProperty("fileName", fileName);
return new TaskResult(true, "OK", "Configuration apply is pending", revision, config,
List.copyOf(changes), true, false, restartConnector);
}

private static TaskResult quick(String preset, Map<String, String> options, String revision,
List<String> changes, boolean reloaded) {
return quick(preset, options, revision, changes, reloaded, false);
}

private static TaskResult quick(String preset, Map<String, String> options, String revision,
List<String> changes, boolean reloaded, boolean restartConnector) {
JsonObject config = new JsonObject();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonParser;

/**
* Typed, read-only data surface used by VotingPlugin Control.
Expand All @@ -43,7 +44,7 @@
public final class ControlInspectionService {
public static final int SCHEMA_VERSION = 1;
public static final int MAX_DATA_BYTES = 512 * 1024;
private static final int MAX_ROWS = 100;
static final int MAX_ROWS = 100;
private static final int MAX_TOP_ROWS = 20;
private static final Comparator<ServiceHealth> SERVICE_HEALTH_ORDER = Comparator
.comparingLong(ServiceHealth::lastVoteTime).reversed()
Expand Down Expand Up @@ -280,6 +281,18 @@ private JsonObject player(JsonObject filters) {
result.add("lastVotes", lastVotes);
result.addProperty("lastVotesTruncated", lastVoteSnapshot.size() > MAX_ROWS);
result.addProperty("pendingOfflineVotes", Math.min(user.getOfflineVotes().size(), 100000));
try {
JsonObject exact = JsonParser.parseString(new ControlPlayerDataService(plugin).readLoaded(user).content())
.getAsJsonObject();
result.addProperty("storageRowAvailable", true);
result.addProperty("storage", exact.get("storage").getAsString());
result.add("columns", exact.getAsJsonArray("columns"));
result.addProperty("columnsTruncated", exact.get("columnsTruncated").getAsBoolean());
Comment thread
BenCodez marked this conversation as resolved.
} catch (java.io.IOException failure) {
result.addProperty("storageRowAvailable", false);
result.add("columns", new JsonArray());
result.addProperty("columnsTruncated", false);
}
return result;
}

Expand All @@ -302,6 +315,7 @@ private JsonObject voteLogSummary(JsonObject filters) {
.forEach(count -> {
JsonObject row = new JsonObject();
row.addProperty("service", safe(count.service, 64));
row.addProperty("count", count.votes);
row.addProperty("votes", count.votes);
services.add(row);
});
Expand All @@ -313,6 +327,7 @@ private JsonObject voteLogSummary(JsonObject filters) {
.forEach(count -> {
JsonObject row = new JsonObject();
row.addProperty("server", safe(count.server, 64));
row.addProperty("count", count.votes);
row.addProperty("votes", count.votes);
servers.add(row);
});
Expand Down Expand Up @@ -440,14 +455,17 @@ private JsonObject diagnostics(JsonObject filters) {
result.addProperty("profile", safe(plugin.getProfile(), 80));
result.addProperty("javaVersion", safe(System.getProperty("java.version", "unknown"), 80));
result.addProperty("backgroundTaskSeconds", plugin.getLastBackgroundTaskTimeTaken());
JsonArray detected = new JsonArray();
Plugin[] plugins = plugin.getServer().getPluginManager().getPlugins();
java.util.Arrays.stream(plugins).map(installed -> installed.getDescription().getName())
.filter(name -> name != null && !name.isBlank()).distinct().sorted(String.CASE_INSENSITIVE_ORDER)
.limit(128).forEach(name -> detected.add(safe(name, 80)));
List<String> detectedNames = java.util.Arrays.stream(plugins)
.map(installed -> installed.getDescription().getName())
.filter(name -> name != null && !name.isBlank()).distinct()
.sorted(String.CASE_INSENSITIVE_ORDER.thenComparing(Comparator.naturalOrder())).toList();
JsonArray detected = new JsonArray();
detectedNames.stream().limit(MAX_ROWS).forEach(name -> detected.add(safe(name, 80)));
result.add("detectedPlugins", detected);
result.addProperty("detectedPluginsTruncated", detectedNames.size() > MAX_ROWS);
JsonArray redacted = new JsonArray();
List.of("credentials", "database hosts and credentials", "Redis/MQTT hosts and credentials",
List.of("credentials", "database and transport hosts and credentials", "Control endpoints and paths",
"webhook URLs", "raw configuration", "raw logs", "player records")
.forEach(redacted::add);
result.add("omittedSensitiveData", redacted);
Expand Down
Loading
Loading