aboutsummaryrefslogtreecommitdiff
path: root/spark-common/src/main/java/me/lucko/spark/common/CommandHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'spark-common/src/main/java/me/lucko/spark/common/CommandHandler.java')
-rw-r--r--spark-common/src/main/java/me/lucko/spark/common/CommandHandler.java372
1 files changed, 372 insertions, 0 deletions
diff --git a/spark-common/src/main/java/me/lucko/spark/common/CommandHandler.java b/spark-common/src/main/java/me/lucko/spark/common/CommandHandler.java
new file mode 100644
index 0000000..898bba7
--- /dev/null
+++ b/spark-common/src/main/java/me/lucko/spark/common/CommandHandler.java
@@ -0,0 +1,372 @@
+package me.lucko.spark.common;
+
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.SetMultimap;
+import com.google.common.collect.Sets;
+
+import me.lucko.spark.common.http.Bytebin;
+import me.lucko.spark.profiler.Sampler;
+import me.lucko.spark.profiler.SamplerBuilder;
+import me.lucko.spark.profiler.ThreadDumper;
+import me.lucko.spark.profiler.ThreadGrouper;
+import me.lucko.spark.profiler.TickCounter;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.Timer;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+/**
+ * Abstract command handling class used by all platforms.
+ *
+ * @param <T> the sender (e.g. CommandSender) type used by the platform
+ */
+public abstract class CommandHandler<T> {
+
+ /** The URL of the viewer frontend */
+ private static final String VIEWER_URL = "https://sparkprofiler.github.io/?";
+ /** The prefix used in all messages */
+ private static final String PREFIX = "&8[&fspark&8] &7";
+
+ /**
+ * The {@link Timer} being used by the {@link #activeSampler}.
+ */
+ private final Timer samplingThread = new Timer("spark-sampling-thread", true);
+
+ /** Guards {@link #activeSampler} */
+ private final Object[] activeSamplerMutex = new Object[0];
+ /** The WarmRoast instance currently running, if any */
+ private Sampler activeSampler = null;
+ /** The tick monitor instance currently running, if any */
+ private ReportingTickMonitor activeTickMonitor = null;
+
+
+ // abstract methods implemented by each platform
+
+ protected abstract void sendMessage(T sender, String message);
+ protected abstract void sendMessage(String message);
+ protected abstract void sendLink(String url);
+ protected abstract void runAsync(Runnable r);
+ protected abstract ThreadDumper getDefaultThreadDumper();
+ protected abstract TickCounter newTickCounter();
+
+ private void sendPrefixedMessage(T sender, String message) {
+ sendMessage(sender, PREFIX + message);
+ }
+
+ private void sendPrefixedMessage(String message) {
+ sendMessage(PREFIX + message);
+ }
+
+ public void handleCommand(T sender, String[] args) {
+ try {
+ if (args.length == 0) {
+ sendInfo(sender);
+ return;
+ }
+
+ List<String> arguments = new ArrayList<>(Arrays.asList(args));
+ switch (arguments.remove(0).toLowerCase()) {
+ case "start":
+ handleStart(sender, arguments);
+ break;
+ case "info":
+ handleInfo(sender);
+ break;
+ case "cancel":
+ handleCancel(sender);
+ break;
+ case "stop":
+ case "upload":
+ case "paste":
+ handleStop(sender);
+ break;
+ case "monitoring":
+ handleMonitoring(sender, arguments);
+ break;
+ default:
+ sendInfo(sender);
+ break;
+ }
+ } catch (IllegalArgumentException e) {
+ sendMessage(sender, "&c" + e.getMessage());
+ }
+ }
+
+ private void sendInfo(T sender) {
+ sendPrefixedMessage(sender, "&fspark profiler &7v1.0");
+ sendMessage(sender, "&b&l> &7/profiler start");
+ sendMessage(sender, " &8[&7--timeout&8 <timeout seconds>]");
+ sendMessage(sender, " &8[&7--thread&8 <thread name>]");
+ sendMessage(sender, " &8[&7--not-combined]");
+ sendMessage(sender, " &8[&7--interval&8 <interval millis>]");
+ sendMessage(sender, " &8[&7--only-ticks-over&8 <tick length millis>]");
+ sendMessage(sender, "&b&l> &7/profiler info");
+ sendMessage(sender, "&b&l> &7/profiler stop");
+ sendMessage(sender, "&b&l> &7/profiler cancel");
+ sendMessage(sender, "&b&l> &7/profiler monitoring");
+ sendMessage(sender, " &8[&7--threshold&8 <percentage increase>]");
+ }
+
+ private void handleStart(T sender, List<String> args) {
+ SetMultimap<String, String> arguments = parseArguments(args);
+
+ int timeoutSeconds = parseInt(arguments, "timeout", "d");
+ if (timeoutSeconds != -1 && timeoutSeconds <= 10) {
+ sendPrefixedMessage(sender, "&cThe specified timeout is not long enough for accurate results to be formed. Please choose a value greater than 10.");
+ return;
+ }
+
+ if (timeoutSeconds != -1 && timeoutSeconds < 30) {
+ sendPrefixedMessage(sender, "&7The accuracy of the output will significantly improve when sampling is able to run for longer periods. Consider setting a timeout value over 30 seconds.");
+ }
+
+ int intervalMillis = parseInt(arguments, "interval", "i");
+ if (intervalMillis <= 0) {
+ intervalMillis = 4;
+ }
+
+ Set<String> threads = Sets.union(arguments.get("thread"), arguments.get("t"));
+ ThreadDumper threadDumper;
+ if (threads.isEmpty()) {
+ // use the server thread
+ threadDumper = getDefaultThreadDumper();
+ } else if (threads.contains("*")) {
+ threadDumper = ThreadDumper.ALL;
+ } else {
+ threadDumper = new ThreadDumper.Specific(threads);
+ }
+
+ ThreadGrouper threadGrouper;
+ if (arguments.containsKey("not-combined")) {
+ threadGrouper = ThreadGrouper.BY_NAME;
+ } else {
+ threadGrouper = ThreadGrouper.BY_POOL;
+ }
+
+ int ticksOver = parseInt(arguments, "only-ticks-over", "o");
+ TickCounter tickCounter = null;
+ if (ticksOver != -1) {
+ try {
+ tickCounter = newTickCounter();
+ } catch (UnsupportedOperationException e) {
+ sendPrefixedMessage(sender, "&cTick counting is not supported on BungeeCord!");
+ return;
+ }
+ }
+
+ Sampler sampler;
+ synchronized (this.activeSamplerMutex) {
+ if (this.activeSampler != null) {
+ sendPrefixedMessage(sender, "&7An active sampler is already running.");
+ return;
+ }
+
+ sendPrefixedMessage("&7Initializing a new profiler, please wait...");
+
+ SamplerBuilder builder = new SamplerBuilder();
+ builder.threadDumper(threadDumper);
+ builder.threadGrouper(threadGrouper);
+ if (timeoutSeconds != -1) {
+ builder.completeAfter(timeoutSeconds, TimeUnit.SECONDS);
+ }
+ builder.samplingInterval(intervalMillis);
+ if (ticksOver != -1) {
+ builder.ticksOver(ticksOver, tickCounter);
+ }
+ sampler = this.activeSampler = builder.start(this.samplingThread);
+
+ sendPrefixedMessage("&bProfiler now active!");
+ if (timeoutSeconds == -1) {
+ sendPrefixedMessage("&7Use '/profiler stop' to stop profiling and upload the results.");
+ } else {
+ sendPrefixedMessage("&7The results will be automatically returned after the profiler has been running for " + timeoutSeconds + " seconds.");
+ }
+ }
+
+ CompletableFuture<Sampler> future = sampler.getFuture();
+
+ // send message if profiling fails
+ future.whenCompleteAsync((s, throwable) -> {
+ if (throwable != null) {
+ sendPrefixedMessage("&cSampling operation failed unexpectedly. Error: " + throwable.toString());
+ throwable.printStackTrace();
+ }
+ });
+
+ // set activeSampler to null when complete.
+ future.whenCompleteAsync((s, throwable) -> {
+ synchronized (this.activeSamplerMutex) {
+ if (sampler == this.activeSampler) {
+ this.activeSampler = null;
+ }
+ }
+ });
+
+ // await the result
+ if (timeoutSeconds != -1) {
+ future.thenAcceptAsync(s -> {
+ sendPrefixedMessage("&7The active sampling operation has completed! Uploading results...");
+ handleUpload(s);
+ });
+ }
+ }
+
+ private void handleInfo(T sender) {
+ synchronized (this.activeSamplerMutex) {
+ if (this.activeSampler == null) {
+ sendPrefixedMessage(sender, "&7There isn't an active sampling task running.");
+ } else {
+ long timeout = this.activeSampler.getEndTime();
+ if (timeout == -1) {
+ sendPrefixedMessage(sender, "&7There is an active sampler currently running, with no defined timeout.");
+ } else {
+ long timeoutDiff = (timeout - System.currentTimeMillis()) / 1000L;
+ sendPrefixedMessage(sender, "&7There is an active sampler currently running, due to timeout in " + timeoutDiff + " seconds.");
+ }
+
+ long runningTime = (System.currentTimeMillis() - this.activeSampler.getStartTime()) / 1000L;
+ sendPrefixedMessage(sender, "&7It has been sampling for " + runningTime + " seconds so far.");
+ }
+ }
+ }
+
+ private void handleStop(T sender) {
+ synchronized (this.activeSamplerMutex) {
+ if (this.activeSampler == null) {
+ sendPrefixedMessage(sender, "&7There isn't an active sampling task running.");
+ } else {
+ this.activeSampler.cancel();
+ sendPrefixedMessage("&7The active sampling operation has been stopped! Uploading results...");
+ handleUpload(this.activeSampler);
+ this.activeSampler = null;
+ }
+ }
+ }
+
+ private void handleCancel(T sender) {
+ synchronized (this.activeSamplerMutex) {
+ if (this.activeSampler == null) {
+ sendPrefixedMessage(sender, "&7There isn't an active sampling task running.");
+ } else {
+ this.activeSampler.cancel();
+ this.activeSampler = null;
+ sendPrefixedMessage("&bThe active sampling task has been cancelled.");
+ }
+ }
+ }
+
+ private void handleUpload(Sampler sampler) {
+ runAsync(() -> {
+ byte[] output = sampler.formCompressedDataPayload();
+ try {
+ String pasteId = Bytebin.postCompressedContent(output);
+ sendPrefixedMessage("&bSampling results:");
+ sendLink(VIEWER_URL + pasteId);
+ } catch (IOException e) {
+ sendPrefixedMessage("&cAn error occurred whilst uploading the results.");
+ e.printStackTrace();
+ }
+ });
+ }
+
+ private void handleMonitoring(T sender, List<String> args) {
+ SetMultimap<String, String> arguments = parseArguments(args);
+
+ if (this.activeTickMonitor == null) {
+
+ int threshold = parseInt(arguments, "threshold", "t");
+ if (threshold == -1) {
+ threshold = 100;
+ }
+
+ try {
+ TickCounter tickCounter = newTickCounter();
+ this.activeTickMonitor = new ReportingTickMonitor(tickCounter, threshold);
+ } catch (UnsupportedOperationException e) {
+ sendPrefixedMessage(sender, "&cNot supported on BungeeCord!");
+ }
+ } else {
+ this.activeTickMonitor.close();
+ this.activeTickMonitor = null;
+ sendPrefixedMessage("&7Tick monitor disabled.");
+ }
+ }
+
+ private class ReportingTickMonitor extends TickMonitor {
+ public ReportingTickMonitor(TickCounter tickCounter, int percentageChangeThreshold) {
+ super(tickCounter, percentageChangeThreshold);
+ }
+
+ @Override
+ protected void sendMessage(String message) {
+ sendPrefixedMessage(message);
+ }
+ }
+
+ private int parseInt(SetMultimap<String, String> arguments, String longArg, String shortArg) {
+ Iterator<String> it = Sets.union(arguments.get(longArg), arguments.get(shortArg)).iterator();
+ if (it.hasNext()) {
+ try {
+ return Math.abs(Integer.parseInt(it.next()));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid input for '" + longArg + "' argument. Please specify a number!");
+ }
+ }
+ return -1; // undefined
+ }
+
+ private static final Pattern FLAG_REGEX = Pattern.compile("--(.+)$|-([a-zA-z])$");
+
+ private static SetMultimap<String, String> parseArguments(List<String> args) {
+ SetMultimap<String, String> arguments = HashMultimap.create();
+
+ String flag = null;
+ List<String> value = null;
+
+ for (int i = 0; i < args.size(); i++) {
+ String arg = args.get(i);
+
+ Matcher matcher = FLAG_REGEX.matcher(arg);
+ boolean matches = matcher.matches();
+
+ if (flag == null || matches) {
+ if (!matches) {
+ throw new IllegalArgumentException("Expected flag at position " + i + " but got '" + arg + "' instead!");
+ }
+
+ String match = matcher.group(1);
+ if (match == null) {
+ match = matcher.group(2);
+ }
+
+ // store existing value, if present
+ if (flag != null) {
+ arguments.put(flag, value.stream().collect(Collectors.joining(" ")));
+ }
+
+ flag = match.toLowerCase();
+ value = new ArrayList<>();
+ } else {
+ // part of a value
+ value.add(arg);
+ }
+ }
+
+ // store remaining value, if present
+ if (flag != null) {
+ arguments.put(flag, value.stream().collect(Collectors.joining(" ")));
+ }
+
+ return arguments;
+ }
+
+}