aboutsummaryrefslogtreecommitdiff
path: root/common/src/main/java/me/lucko
diff options
context:
space:
mode:
authorLuck <git@lucko.me>2018-05-26 22:52:58 +0100
committerLuck <git@lucko.me>2018-05-27 00:02:02 +0100
commit429eeb35876576d861404cd199b6e9763fc4e5b0 (patch)
tree624e1c05433e8ab5775a0177ecf5d5982de54805 /common/src/main/java/me/lucko
parent3fe5e5517b1c529d95cf9f43fd8420c66db0092a (diff)
downloadspark-429eeb35876576d861404cd199b6e9763fc4e5b0.tar.gz
spark-429eeb35876576d861404cd199b6e9763fc4e5b0.tar.bz2
spark-429eeb35876576d861404cd199b6e9763fc4e5b0.zip
Initial commit for spark
Diffstat (limited to 'common/src/main/java/me/lucko')
-rw-r--r--common/src/main/java/me/lucko/spark/common/CommandHandler.java287
-rw-r--r--common/src/main/java/me/lucko/spark/common/SamplerBuilder.java44
-rw-r--r--common/src/main/java/me/lucko/spark/common/http/Bytebin.java69
-rw-r--r--common/src/main/java/me/lucko/spark/common/http/HttpClient.java113
4 files changed, 513 insertions, 0 deletions
diff --git a/common/src/main/java/me/lucko/spark/common/CommandHandler.java b/common/src/main/java/me/lucko/spark/common/CommandHandler.java
new file mode 100644
index 0000000..aa7e47d
--- /dev/null
+++ b/common/src/main/java/me/lucko/spark/common/CommandHandler.java
@@ -0,0 +1,287 @@
+package me.lucko.spark.common;
+
+import com.google.gson.JsonObject;
+import com.sk89q.warmroast.ThreadDumper;
+import com.sk89q.warmroast.Sampler;
+
+import me.lucko.spark.common.http.Bytebin;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+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 {@link Timer} being used by the {@link #activeSampler}.
+ */
+ private final Timer timer = 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;
+
+
+ // abstract methods implemented by each platform
+
+ protected abstract void sendMessage(T sender, String message);
+ protected abstract void runAsync(Runnable r);
+
+ private void sendPrefixedMessage(T sender, String message) {
+ sendMessage(sender, "&8[&fspark&8] &7" + 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;
+ 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--interval&8 <interval millis>]");
+ sendMessage(sender, "&b&l> &7/profiler info");
+ sendMessage(sender, "&b&l> &7/profiler stop");
+ sendMessage(sender, "&b&l> &7/profiler cancel");
+ }
+
+ private void handleStart(T sender, List<String> args) {
+ Map<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.");
+ return;
+ }
+
+ if (timeoutSeconds != -1 && timeoutSeconds < 100) {
+ sendPrefixedMessage(sender, "&7The accuracy of the output will significantly improve when sampling is able to run for longer periods. Consider setting a value of timeout over 1-2 minutes.");
+ }
+
+ int intervalMillis = parseInt(arguments, "interval", "i");
+ if (intervalMillis == -1) {
+ intervalMillis = 10;
+ }
+
+ String threadName = arguments.getOrDefault("thread", arguments.getOrDefault("t", null));
+ ThreadDumper threadDumper;
+ if (threadName == null) {
+ // use the server thread
+ threadDumper = new ThreadDumper.Specific(new long[]{Thread.currentThread().getId()});
+ } else if (threadName.equals("*")) {
+ threadDumper = new ThreadDumper.All();
+ } else {
+ threadDumper = new ThreadDumper.Specific(threadName);
+ }
+
+ Sampler sampler;
+ synchronized (this.activeSamplerMutex) {
+ if (this.activeSampler != null) {
+ sendPrefixedMessage(sender, "&7An active sampler is already running.");
+ return;
+ }
+
+ sendPrefixedMessage(sender, "&7Starting a new sampler task...");
+
+ SamplerBuilder builder = new SamplerBuilder();
+ builder.threadDumper(threadDumper);
+ if (timeoutSeconds != -1) {
+ builder.completeAfter(timeoutSeconds, TimeUnit.SECONDS);
+ }
+ builder.samplingInterval(intervalMillis);
+ sampler = this.activeSampler = builder.start(timer);
+
+ sendPrefixedMessage(sender, "&bSampling has begun!");
+ }
+
+ CompletableFuture<Sampler> future = sampler.getFuture();
+
+ // send message if profiling fails
+ future.whenComplete((s, throwable) -> {
+ if (throwable != null) {
+ sendPrefixedMessage(sender, "&cSampling operation failed unexpectedly. Error: " + throwable.toString());
+ throwable.printStackTrace();
+ }
+ });
+
+ // set activeSampler to null when complete.
+ future.whenComplete((s, throwable) -> {
+ synchronized (this.activeSamplerMutex) {
+ if (sampler == this.activeSampler) {
+ this.activeSampler = null;
+ }
+ }
+ });
+
+ // await the result
+ if (timeoutSeconds != -1) {
+ future.thenAcceptAsync(s -> {
+ sendPrefixedMessage(sender, "&7The active sampling operation has completed! Uploading results...");
+ handleUpload(sender, 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(sender, "&7The active sampling operation has been stopped! Uploading results...");
+ handleUpload(sender, 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(sender, "&bThe active sampling task has been cancelled.");
+ }
+ }
+ }
+
+ private void handleUpload(T sender, Sampler sampler) {
+ runAsync(() -> {
+ JsonObject output = sampler.formOutput();
+ try {
+ String pasteId = Bytebin.postContent(output);
+ sendPrefixedMessage(sender, "&bSampling results can be viewed here: &7" + VIEWER_URL + pasteId);
+ } catch (IOException e) {
+ sendPrefixedMessage(sender, "&cAn error occurred whilst uploading the results.");
+ e.printStackTrace();
+ }
+ });
+ }
+
+ private int parseInt(Map<String, String> arguments, String longArg, String shortArg) {
+ String value = arguments.getOrDefault(longArg, arguments.getOrDefault(shortArg, null));
+ if (value != null) {
+ try {
+ return Math.abs(Integer.parseInt(value));
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Invalid input for '" + longArg + "' argument. Please specify a number!");
+ }
+ } else {
+ return -1; // undefined
+ }
+ }
+
+ private static final Pattern FLAG_REGEX = Pattern.compile("--(.+)$|-([a-zA-z])$");
+
+ private static Map<String, String> parseArguments(List<String> args) {
+ Map<String, String> arguments = new HashMap<>();
+
+ 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;
+ }
+
+}
diff --git a/common/src/main/java/me/lucko/spark/common/SamplerBuilder.java b/common/src/main/java/me/lucko/spark/common/SamplerBuilder.java
new file mode 100644
index 0000000..e6c0cf8
--- /dev/null
+++ b/common/src/main/java/me/lucko/spark/common/SamplerBuilder.java
@@ -0,0 +1,44 @@
+package me.lucko.spark.common;
+
+import com.google.common.base.Preconditions;
+import com.sk89q.warmroast.ThreadDumper;
+import com.sk89q.warmroast.Sampler;
+
+import java.util.Timer;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Builds {@link Sampler} instances.
+ */
+public class SamplerBuilder {
+
+ private int samplingInterval = 10;
+ private long timeout = -1;
+ private ThreadDumper threadDumper = new ThreadDumper.All();
+
+ public SamplerBuilder() {
+ }
+
+ public SamplerBuilder samplingInterval(int samplingInterval) {
+ this.samplingInterval = samplingInterval;
+ return this;
+ }
+
+ public SamplerBuilder completeAfter(long timeout, TimeUnit unit) {
+ Preconditions.checkArgument(timeout > 0, "time > 0");
+ this.timeout = System.currentTimeMillis() + unit.toMillis(timeout);
+ return this;
+ }
+
+ public SamplerBuilder threadDumper(ThreadDumper threadDumper) {
+ this.threadDumper = threadDumper;
+ return this;
+ }
+
+ public Sampler start(Timer timer) {
+ Sampler sampler = new Sampler(samplingInterval, threadDumper, timeout);
+ sampler.start(timer);
+ return sampler;
+ }
+
+}
diff --git a/common/src/main/java/me/lucko/spark/common/http/Bytebin.java b/common/src/main/java/me/lucko/spark/common/http/Bytebin.java
new file mode 100644
index 0000000..7d95838
--- /dev/null
+++ b/common/src/main/java/me/lucko/spark/common/http/Bytebin.java
@@ -0,0 +1,69 @@
+package me.lucko.spark.common.http;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+import okhttp3.MediaType;
+import okhttp3.Request;
+import okhttp3.RequestBody;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import java.io.BufferedReader;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.nio.charset.StandardCharsets;
+import java.util.zip.GZIPOutputStream;
+
+/**
+ * Utility for uploading JSON data to bytebin.
+ */
+public final class Bytebin {
+
+ /** Shared GSON instance */
+ private static final Gson GSON = new GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create();
+ /** Media type for JSON data */
+ private static final MediaType JSON_TYPE = MediaType.parse("application/json; charset=utf-8");
+ /** The URL used to upload sampling data */
+ private static final String UPLOAD_ENDPOINT = "https://bytebin.lucko.me/post";
+
+ public static String postContent(JsonElement content) throws IOException {
+ ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
+ try (Writer writer = new OutputStreamWriter(new GZIPOutputStream(byteOut), StandardCharsets.UTF_8)) {
+ GSON.toJson(content, writer);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ RequestBody body = RequestBody.create(JSON_TYPE, byteOut.toByteArray());
+
+ Request.Builder requestBuilder = new Request.Builder()
+ .url(UPLOAD_ENDPOINT)
+ .header("Content-Encoding", "gzip")
+ .post(body);
+
+ Request request = requestBuilder.build();
+ try (Response response = HttpClient.makeCall(request)) {
+ try (ResponseBody responseBody = response.body()) {
+ if (responseBody == null) {
+ throw new RuntimeException("No response");
+ }
+
+ try (InputStream inputStream = responseBody.byteStream()) {
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
+ JsonObject object = new Gson().fromJson(reader, JsonObject.class);
+ return object.get("key").getAsString();
+ }
+ }
+ }
+ }
+ }
+
+ private Bytebin() {}
+}
diff --git a/common/src/main/java/me/lucko/spark/common/http/HttpClient.java b/common/src/main/java/me/lucko/spark/common/http/HttpClient.java
new file mode 100644
index 0000000..61db597
--- /dev/null
+++ b/common/src/main/java/me/lucko/spark/common/http/HttpClient.java
@@ -0,0 +1,113 @@
+/*
+ * This file is part of LuckPerms, licensed under the MIT License.
+ *
+ * Copyright (c) lucko (Luck) <luck@lucko.me>
+ * Copyright (c) contributors
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+package me.lucko.spark.common.http;
+
+import okhttp3.Interceptor;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+import okhttp3.ResponseBody;
+
+import java.io.IOException;
+import java.net.Proxy;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utility class for making http requests.
+ */
+public final class HttpClient {
+ private static OkHttpClient client = null;
+
+ private static synchronized OkHttpClient getClient() {
+ if (client == null) {
+ client = new OkHttpClient.Builder()
+ .proxySelector(new NullSafeProxySelector())
+ .addInterceptor(new UserAgentInterceptor())
+ .build();
+ }
+ return client;
+ }
+
+ public static Response makeCall(Request request) throws IOException {
+ Response response = getClient().newCall(request).execute();
+ if (!response.isSuccessful()) {
+ throw exceptionForUnsuccessfulResponse(response);
+ }
+ return response;
+ }
+
+ private static RuntimeException exceptionForUnsuccessfulResponse(Response response) {
+ String msg = "";
+ try (ResponseBody responseBody = response.body()) {
+ if (responseBody != null) {
+ msg = responseBody.string();
+ }
+ } catch (IOException e) {
+ // ignore
+ }
+ return new RuntimeException("Got response: " + response.code() + " - " + response.message() + " - " + msg);
+ }
+
+ private static final class UserAgentInterceptor implements Interceptor {
+ @Override
+ public Response intercept(Chain chain) throws IOException {
+ Request orig = chain.request();
+ Request modified = orig.newBuilder()
+ .header("User-Agent", "spark-plugin")
+ .build();
+
+ return chain.proceed(modified);
+ }
+ }
+
+ // sometimes ProxySelector#getDefault returns null, and okhttp doesn't like that
+ private static final class NullSafeProxySelector extends ProxySelector {
+ private static final List<Proxy> DIRECT = Collections.singletonList(Proxy.NO_PROXY);
+
+ @Override
+ public List<Proxy> select(URI uri) {
+ ProxySelector def = ProxySelector.getDefault();
+ if (def == null) {
+ return DIRECT;
+ }
+ return def.select(uri);
+ }
+
+ @Override
+ public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
+ ProxySelector def = ProxySelector.getDefault();
+ if (def != null) {
+ def.connectFailed(uri, sa, ioe);
+ }
+ }
+ }
+
+ private HttpClient() {}
+} \ No newline at end of file