Skip to content

Commit d404825

Browse files
committed
Merge remote-tracking branch 'upstream/master'
2 parents f6d27bf + 7e3a173 commit d404825

19 files changed

Lines changed: 453 additions & 340 deletions

File tree

build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ subprojects {
2121
ext {
2222
baseVersion = '1.10'
2323
patchVersion = ''
24-
buildCommit = 'dev-afb2c92-SNAPSHOT'
24+
buildCommit = 'dev-7e3a173-SNAPSHOT'
2525
pluginVersion = baseVersion + '.' + patchVersion
2626
pluginDescription = 'spark is a performance profiling plugin/mod for Minecraft clients, servers and proxies.'
2727

spark-common/src/main/java/me/lucko/spark/common/SparkPlatform.java

Lines changed: 12 additions & 288 deletions
Large diffs are not rendered by default.
Lines changed: 340 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,340 @@
1+
/*
2+
* This file is part of spark.
3+
*
4+
* Copyright (c) lucko (Luck) <luck@lucko.me>
5+
* Copyright (c) contributors
6+
*
7+
* This program is free software: you can redistribute it and/or modify
8+
* it under the terms of the GNU General Public License as published by
9+
* the Free Software Foundation, either version 3 of the License, or
10+
* (at your option) any later version.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU General Public License
18+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19+
*/
20+
21+
package me.lucko.spark.common.command;
22+
23+
import com.google.common.collect.ImmutableList;
24+
import me.lucko.spark.common.SparkPlatform;
25+
import me.lucko.spark.common.SparkPlugin;
26+
import me.lucko.spark.common.command.modules.ActivityLogModule;
27+
import me.lucko.spark.common.command.modules.GcMonitoringModule;
28+
import me.lucko.spark.common.command.modules.HealthModule;
29+
import me.lucko.spark.common.command.modules.HeapAnalysisModule;
30+
import me.lucko.spark.common.command.modules.SamplerModule;
31+
import me.lucko.spark.common.command.modules.TickMonitoringModule;
32+
import me.lucko.spark.common.command.sender.CommandSender;
33+
import me.lucko.spark.common.command.tabcomplete.CompletionSupplier;
34+
import me.lucko.spark.common.command.tabcomplete.TabCompleter;
35+
import me.lucko.spark.common.util.config.Configuration;
36+
import net.kyori.adventure.text.Component;
37+
import net.kyori.adventure.text.event.ClickEvent;
38+
39+
import java.util.ArrayList;
40+
import java.util.Arrays;
41+
import java.util.Collections;
42+
import java.util.LinkedHashMap;
43+
import java.util.List;
44+
import java.util.Map;
45+
import java.util.Set;
46+
import java.util.concurrent.CompletableFuture;
47+
import java.util.concurrent.atomic.AtomicBoolean;
48+
import java.util.concurrent.atomic.AtomicReference;
49+
import java.util.concurrent.locks.ReentrantLock;
50+
import java.util.logging.Level;
51+
import java.util.stream.Collectors;
52+
import java.util.stream.Stream;
53+
54+
import static net.kyori.adventure.text.Component.space;
55+
import static net.kyori.adventure.text.Component.text;
56+
import static net.kyori.adventure.text.format.NamedTextColor.GOLD;
57+
import static net.kyori.adventure.text.format.NamedTextColor.GRAY;
58+
import static net.kyori.adventure.text.format.NamedTextColor.RED;
59+
import static net.kyori.adventure.text.format.NamedTextColor.WHITE;
60+
import static net.kyori.adventure.text.format.TextDecoration.BOLD;
61+
62+
public class CommandManager implements AutoCloseable {
63+
private final SparkPlatform platform;
64+
65+
private final boolean disableResponseBroadcast;
66+
private final List<CommandModule> modules;
67+
private final List<Command> commands;
68+
private final ReentrantLock executeLock = new ReentrantLock(true);
69+
70+
public CommandManager(SparkPlatform platform, Configuration configuration) {
71+
this.platform = platform;
72+
73+
this.disableResponseBroadcast = configuration.getBoolean("disableResponseBroadcast", false);
74+
75+
this.modules = new ArrayList<>();
76+
this.modules.add(new SamplerModule());
77+
this.modules.add(new HealthModule());
78+
if (platform.getTickHook() != null) {
79+
this.modules.add(new TickMonitoringModule());
80+
}
81+
this.modules.add(new GcMonitoringModule());
82+
this.modules.add(new HeapAnalysisModule());
83+
this.modules.add(new ActivityLogModule());
84+
85+
ImmutableList.Builder<Command> commandsBuilder = ImmutableList.builder();
86+
for (CommandModule module : this.modules) {
87+
module.registerCommands(commandsBuilder::add);
88+
}
89+
this.commands = commandsBuilder.build();
90+
}
91+
92+
@Override
93+
public void close() {
94+
for (CommandModule module : this.modules) {
95+
module.close();
96+
}
97+
}
98+
99+
public boolean shouldBroadcastResponse() {
100+
return !this.disableResponseBroadcast;
101+
}
102+
103+
public List<Command> getCommands() {
104+
return this.commands;
105+
}
106+
107+
private List<Command> getAvailableCommands(CommandSender sender) {
108+
if (sender.hasPermission("spark")) {
109+
return this.commands;
110+
}
111+
return this.commands.stream()
112+
.filter(c -> sender.hasPermission("spark." + c.primaryAlias()))
113+
.collect(Collectors.toList());
114+
}
115+
116+
public Set<String> getAllSparkPermissions() {
117+
return Stream.concat(
118+
Stream.of("spark"),
119+
this.commands.stream()
120+
.map(Command::primaryAlias)
121+
.map(alias -> "spark." + alias)
122+
).collect(Collectors.toSet());
123+
}
124+
125+
public boolean hasPermissionForAnyCommand(CommandSender sender) {
126+
return !getAvailableCommands(sender).isEmpty();
127+
}
128+
129+
public CompletableFuture<Void> executeCommand(CommandSender sender, String[] args) {
130+
CompletableFuture<Void> future = new CompletableFuture<>();
131+
AtomicReference<Thread> executorThread = new AtomicReference<>();
132+
AtomicReference<Thread> timeoutThread = new AtomicReference<>();
133+
AtomicBoolean completed = new AtomicBoolean(false);
134+
135+
SparkPlugin plugin = this.platform.getPlugin();
136+
137+
// execute the command
138+
plugin.executeAsync(() -> {
139+
executorThread.set(Thread.currentThread());
140+
this.executeLock.lock();
141+
try {
142+
executeCommand0(sender, args);
143+
future.complete(null);
144+
} catch (Throwable e) {
145+
plugin.log(Level.SEVERE, "Exception occurred whilst executing a spark command", e);
146+
future.completeExceptionally(e);
147+
} finally {
148+
this.executeLock.unlock();
149+
executorThread.set(null);
150+
completed.set(true);
151+
152+
Thread timeout = timeoutThread.get();
153+
if (timeout != null) {
154+
timeout.interrupt();
155+
}
156+
}
157+
});
158+
159+
// schedule a task to detect timeouts
160+
plugin.executeAsync(() -> {
161+
timeoutThread.set(Thread.currentThread());
162+
int warningIntervalSeconds = 5;
163+
164+
try {
165+
if (completed.get()) {
166+
return;
167+
}
168+
169+
for (int i = 1; i <= 3; i++) {
170+
try {
171+
Thread.sleep(warningIntervalSeconds * 1000);
172+
} catch (InterruptedException e) {
173+
// ignore
174+
}
175+
176+
if (completed.get()) {
177+
return;
178+
}
179+
180+
Thread executor = executorThread.get();
181+
if (executor == null) {
182+
plugin.log(Level.WARNING, "A command execution has not completed after " +
183+
(i * warningIntervalSeconds) + " seconds but there is no executor present. Perhaps the executor shutdown?");
184+
plugin.log(Level.WARNING, "If the command subsequently completes without any errors, this warning should be ignored. :)");
185+
186+
} else {
187+
String stackTrace = Arrays.stream(executor.getStackTrace())
188+
.map(el -> " " + el)
189+
.collect(Collectors.joining("\n"));
190+
191+
plugin.log(Level.WARNING, "A command execution has not completed after " +
192+
(i * warningIntervalSeconds) + " seconds, it *might* be stuck. Trace: \n" + stackTrace);
193+
plugin.log(Level.WARNING, "If the command subsequently completes without any errors, this warning should be ignored. :)");
194+
}
195+
}
196+
} finally {
197+
timeoutThread.set(null);
198+
}
199+
});
200+
201+
return future;
202+
}
203+
204+
private void executeCommand0(CommandSender sender, String[] args) {
205+
CommandResponseHandler resp = new CommandResponseHandler(this.platform, sender);
206+
List<Command> commands = getAvailableCommands(sender);
207+
208+
if (commands.isEmpty()) {
209+
resp.replyPrefixed(text("You do not have permission to use this command.", RED));
210+
return;
211+
}
212+
213+
if (args.length == 0) {
214+
resp.replyPrefixed(text()
215+
.append(text("spark", WHITE))
216+
.append(space())
217+
.append(text("v" + this.platform.getPlugin().getVersion(), GRAY))
218+
.build()
219+
);
220+
221+
String helpCmd = "/" + this.platform.getPlugin().getCommandName() + " help";
222+
resp.replyPrefixed(text()
223+
.color(GRAY)
224+
.append(text("Run "))
225+
.append(text()
226+
.content(helpCmd)
227+
.color(WHITE)
228+
.clickEvent(ClickEvent.runCommand(helpCmd))
229+
.build()
230+
)
231+
.append(text(" to view usage information."))
232+
.build()
233+
);
234+
return;
235+
}
236+
237+
ArrayList<String> rawArgs = new ArrayList<>(Arrays.asList(args));
238+
String alias = rawArgs.remove(0).toLowerCase();
239+
240+
for (Command command : commands) {
241+
if (command.aliases().contains(alias)) {
242+
resp.setCommandPrimaryAlias(command.primaryAlias());
243+
try {
244+
command.executor().execute(this.platform, sender, resp, new Arguments(rawArgs, command.allowSubCommand()));
245+
} catch (Arguments.ParseException e) {
246+
resp.replyPrefixed(text(e.getMessage(), RED));
247+
}
248+
return;
249+
}
250+
}
251+
252+
sendUsage(commands, resp);
253+
}
254+
255+
public List<String> tabCompleteCommand(CommandSender sender, String[] args) {
256+
List<Command> commands = getAvailableCommands(sender);
257+
if (commands.isEmpty()) {
258+
return Collections.emptyList();
259+
}
260+
261+
List<String> arguments = new ArrayList<>(Arrays.asList(args));
262+
263+
if (args.length <= 1) {
264+
List<String> mainCommands = commands.stream()
265+
.map(Command::primaryAlias)
266+
.collect(Collectors.toList());
267+
268+
return TabCompleter.create()
269+
.at(0, CompletionSupplier.startsWith(mainCommands))
270+
.complete(arguments);
271+
}
272+
273+
String alias = arguments.remove(0);
274+
for (Command command : commands) {
275+
if (command.aliases().contains(alias)) {
276+
return command.tabCompleter().completions(this.platform, sender, arguments);
277+
}
278+
}
279+
280+
return Collections.emptyList();
281+
}
282+
283+
private void sendUsage(List<Command> commands, CommandResponseHandler sender) {
284+
sender.replyPrefixed(text()
285+
.append(text("spark", WHITE))
286+
.append(space())
287+
.append(text("v" + this.platform.getPlugin().getVersion(), GRAY))
288+
.build()
289+
);
290+
for (Command command : commands) {
291+
String usage = "/" + this.platform.getPlugin().getCommandName() + " " + command.primaryAlias();
292+
293+
if (command.allowSubCommand()) {
294+
Map<String, List<Command.ArgumentInfo>> argumentsBySubCommand = command.arguments().stream()
295+
.collect(Collectors.groupingBy(Command.ArgumentInfo::subCommandName, LinkedHashMap::new, Collectors.toList()));
296+
297+
argumentsBySubCommand.forEach((subCommand, arguments) -> {
298+
String subCommandUsage = usage + " " + subCommand;
299+
300+
sender.reply(text()
301+
.append(text(">", GOLD, BOLD))
302+
.append(space())
303+
.append(text().content(subCommandUsage).color(GRAY).clickEvent(ClickEvent.suggestCommand(subCommandUsage)).build())
304+
.build()
305+
);
306+
307+
for (Command.ArgumentInfo arg : arguments) {
308+
if (arg.argumentName().isEmpty()) {
309+
continue;
310+
}
311+
sender.reply(arg.toComponent(" "));
312+
}
313+
});
314+
} else {
315+
sender.reply(text()
316+
.append(text(">", GOLD, BOLD))
317+
.append(space())
318+
.append(text().content(usage).color(GRAY).clickEvent(ClickEvent.suggestCommand(usage)).build())
319+
.build()
320+
);
321+
322+
for (Command.ArgumentInfo arg : command.arguments()) {
323+
sender.reply(arg.toComponent(" "));
324+
}
325+
}
326+
}
327+
328+
sender.reply(Component.empty());
329+
sender.replyPrefixed(text()
330+
.append(text("For full usage information, please go to: "))
331+
.append(text()
332+
.content("https://spark.lucko.me/docs/Command-Usage")
333+
.color(WHITE)
334+
.clickEvent(ClickEvent.openUrl("https://spark.lucko.me/docs/Command-Usage"))
335+
.build()
336+
)
337+
.build()
338+
);
339+
}
340+
}

spark-common/src/main/java/me/lucko/spark/common/command/CommandResponseHandler.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,15 +110,15 @@ public void reply(Iterable<Component> message) {
110110
}
111111

112112
public void broadcast(Component message) {
113-
if (this.platform.shouldBroadcastResponse()) {
113+
if (this.platform.getCommandManager().shouldBroadcastResponse()) {
114114
allSenders(sender -> sendMessage(sender, message));
115115
} else {
116116
reply(message);
117117
}
118118
}
119119

120120
public void broadcast(Iterable<Component> message) {
121-
if (this.platform.shouldBroadcastResponse()) {
121+
if (this.platform.getCommandManager().shouldBroadcastResponse()) {
122122
Component joinedMsg = Component.join(JoinConfiguration.separator(Component.newline()), message);
123123
allSenders(sender -> sendMessage(sender, joinedMsg));
124124
} else {

0 commit comments

Comments
 (0)