Upload via GUI (24 Dateien)

This commit is contained in:
2026-11-01 11:16:47 +00:00
parent 257fbc1e55
commit e1c6f8dfca
12 changed files with 255 additions and 11 deletions
@@ -1,6 +1,7 @@
package de.deskhub.minecraft.bukkit;
import de.deskhub.minecraft.core.HeartbeatFile;
import de.deskhub.minecraft.core.OpenTickets;
import de.deskhub.minecraft.core.Settings;
import de.deskhub.minecraft.core.Template;
import de.deskhub.minecraft.core.TicketClient;
@@ -28,6 +29,7 @@ public class BukkitPlugin extends JavaPlugin implements CommandExecutor {
private Settings settings;
private TicketClient client;
private HeartbeatFile heartbeat;
private OpenTickets openTickets;
private BukkitTask beatTask;
@Override
@@ -37,7 +39,9 @@ public class BukkitPlugin extends JavaPlugin implements CommandExecutor {
if (getCommand("deskhubmc") != null) getCommand("deskhubmc").setExecutor(this);
heartbeat = new HeartbeatFile(new File(getDataFolder(), "lebenszeichen.properties"));
HeartbeatFile.Previous previous = heartbeat.readPrevious();
openTickets = new OpenTickets(new File(getDataFolder(), "offene-tickets.properties"));
long now = System.currentTimeMillis();
resolveOpenOnStart(now);
try {
heartbeat.start(now);
} catch (IOException e) {
@@ -70,6 +74,7 @@ public class BukkitPlugin extends JavaPlugin implements CommandExecutor {
// Beim Beenden darf gewartet werden: der Server wartet auf das Plugin, solange es nicht länger als das Zeitlimit dauert
Map<String, String> v = Template.vars(serverName(), "Plugin auf dem Server", now);
TicketResult r = client.create(Template.apply(settings.subjectShutdown, v), Template.apply(settings.messageShutdown, v));
if (r.ok) openTickets.put(serverName(), r.number, now);
getLogger().info(r.ok ? "Ticket " + r.number + " angelegt (Herunterfahren)." : "Ticket nicht angelegt: " + r.error);
}
}
@@ -104,12 +109,42 @@ public class BukkitPlugin extends JavaPlugin implements CommandExecutor {
v.put("lastSeen", previous.lastBeat > 0 ? Template.formatTime(previous.lastBeat) : "unbekannt");
v.put("downtime", previous.lastBeat > 0 ? Template.formatDuration(now - previous.lastBeat) : "unbekannt");
TicketResult r = client.create(Template.apply(settings.subjectCrash, v), Template.apply(settings.messageCrash, v));
if (r.ok) getLogger().info("Ticket " + r.number + " angelegt.");
else getLogger().warning("Ticket nicht angelegt: " + r.error);
if (r.ok) {
getLogger().info("Ticket " + r.number + " angelegt.");
// Der Server läuft wieder (sonst würde dieser Code nicht laufen): Das Ausfall-Ticket gleich lösen
resolveNow(r.number, previous.lastBeat > 0 ? previous.lastBeat : 0, now);
} else getLogger().warning("Ticket nicht angelegt: " + r.error);
}
});
}
/** Beim Start: Ein beim letzten Herunterfahren angelegtes Ticket ist erledigt, der Server läuft wieder. */
private void resolveOpenOnStart(final long now) {
final String number = openTickets.get(serverName());
if (number == null || !settings.resolveOnRecovery || !settings.problems().isEmpty()) return;
final long since = openTickets.since(serverName());
Bukkit.getScheduler().runTaskAsynchronously(this, new Runnable() {
@Override
public void run() {
resolveNow(number, since, now);
}
});
}
/** Blockierend (nur in einem Hintergrund-Thread aufrufen): Ticket auf Gelöst setzen und aus der Merkliste nehmen. */
private void resolveNow(String number, long since, long now) {
if (!settings.resolveOnRecovery || !settings.problems().isEmpty()) return;
Map<String, String> v = Template.vars(serverName(), "Plugin auf dem Server", now);
v.put("downtime", since > 0 ? Template.formatDuration(now - since) : "unbekannt");
TicketResult r = client.resolve(number, Template.apply(settings.messageRecovered, v));
if (r.ok || r.httpCode == 404) {
openTickets.remove(serverName());
getLogger().info(r.ok ? "Ticket " + number + " gelöst." : "Ticket " + number + " gibt es in DeskHub nicht mehr, wird nicht weiter verfolgt.");
} else {
getLogger().warning("Ticket " + number + " konnte nicht gelöst werden: " + r.error + " (bitte von Hand lösen)");
}
}
@Override
public boolean onCommand(final CommandSender sender, Command command, String label, String[] args) {
if (!sender.hasPermission("deskhubmc.admin")) {
@@ -1,5 +1,6 @@
package de.deskhub.minecraft.bungee;
import de.deskhub.minecraft.core.OpenTickets;
import de.deskhub.minecraft.core.OutageTracker;
import de.deskhub.minecraft.core.Settings;
import de.deskhub.minecraft.core.Template;
@@ -36,6 +37,7 @@ public final class BungeePlugin extends Plugin {
private Settings settings;
private TicketClient client;
private OutageTracker tracker;
private OpenTickets openTickets;
private ScheduledTask watchdog;
@Override
@@ -69,6 +71,7 @@ public final class BungeePlugin extends Plugin {
settings = new Settings(new BungeeConfig(new Configuration()));
}
client = new TicketClient(settings, getDescription().getVersion());
openTickets = new OpenTickets(new File(getDataFolder(), "offene-tickets.properties"));
tracker = new OutageTracker(settings.monitorFailures, settings.cooldownMinutes, settings.ignoreNeverOnline);
for (String w : settings.warnings()) getLogger().warning(w);
List<String> problems = settings.problems();
@@ -115,6 +118,7 @@ public final class BungeePlugin extends Plugin {
OutageTracker.Decision d = tracker.report(server, online, now);
if (d == OutageTracker.Decision.RECOVERED) {
getLogger().info("Server \"" + server + "\" ist wieder erreichbar.");
resolveTicket(server, now);
} else if (d == OutageTracker.Decision.OPEN_TICKET) {
final long since = tracker.offlineSince(server);
getLogger().warning("Server \"" + server + "\" ist nicht erreichbar. Es wird ein Ticket angelegt.");
@@ -127,13 +131,37 @@ public final class BungeePlugin extends Plugin {
public void run() {
Map<String, String> v = Template.vars(server, "BungeeCord-Proxy", since > 0 ? since : now);
TicketResult r = client.create(Template.apply(settings.subjectOffline, v), Template.apply(settings.messageOffline, v));
if (r.ok) getLogger().info("Ticket " + r.number + " angelegt.");
if (r.ok) {
openTickets.put(server, r.number, since > 0 ? since : now);
getLogger().info("Ticket " + r.number + " angelegt.");
}
else getLogger().warning("Ticket nicht angelegt: " + r.error);
}
});
}
}
/** Server wieder erreichbar: das beim Ausfall angelegte Ticket in DeskHub auf "Gelöst" setzen. */
private void resolveTicket(final String server, final long now) {
final String number = openTickets.get(server);
if (number == null || !settings.resolveOnRecovery || !settings.problems().isEmpty()) return;
final long since = openTickets.since(server);
getProxy().getScheduler().runAsync(this, new Runnable() {
@Override
public void run() {
Map<String, String> v = Template.vars(server, "BungeeCord-Proxy", now);
v.put("downtime", since > 0 ? Template.formatDuration(now - since) : "unbekannt");
TicketResult r = client.resolve(number, Template.apply(settings.messageRecovered, v));
if (r.ok || r.httpCode == 404) {
openTickets.remove(server);
getLogger().info(r.ok ? "Ticket " + number + " gelöst." : "Ticket " + number + " gibt es in DeskHub nicht mehr, wird nicht weiter verfolgt.");
} else {
getLogger().warning("Ticket " + number + " konnte nicht gelöst werden: " + r.error + " (bitte von Hand lösen)");
}
}
});
}
private final class McCommand extends Command {
McCommand() {
super("deskhubmc", "deskhubmc.admin", "dhmc");
@@ -0,0 +1,78 @@
package de.deskhub.minecraft.core;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Properties;
/**
* Merkt sich je Server die Nummer des Tickets, das für den aktuellen Ausfall angelegt wurde, damit es bei "wieder erreichbar" gelöst
* werden kann. Wird in einer kleinen Datei gespeichert, damit ein Neustart des Proxys während des Ausfalls das Ticket nicht vergisst.
*/
public final class OpenTickets {
private final File file;
private final Properties props = new Properties();
public OpenTickets(File file) {
this.file = file;
if (file.isFile()) {
try {
InputStream in = new FileInputStream(file);
try {
props.load(in);
} finally {
in.close();
}
} catch (IOException e) {
props.clear();
}
}
}
/** Ticketnummer des laufenden Ausfalls oder null. */
public synchronized String get(String server) {
String v = props.getProperty(server);
if (v == null) return null;
int i = v.indexOf(';');
return i < 0 ? v : v.substring(0, i);
}
/** Beginn des Ausfalls in Millisekunden (0 = unbekannt). */
public synchronized long since(String server) {
String v = props.getProperty(server);
int i = v == null ? -1 : v.indexOf(';');
if (i < 0) return 0;
try {
return Long.parseLong(v.substring(i + 1));
} catch (NumberFormatException e) {
return 0;
}
}
public synchronized void put(String server, String number, long sinceMillis) {
props.setProperty(server, number + ";" + sinceMillis);
save();
}
public synchronized void remove(String server) {
if (props.remove(server) != null) save();
}
private void save() {
try {
File dir = file.getParentFile();
if (dir != null && !dir.isDirectory() && !dir.mkdirs()) return;
OutputStream out = new FileOutputStream(file);
try {
props.store(out, "DeskHub-Minecraft: offene Ausfall-Tickets (Server = Ticketnummer;Beginn)");
} finally {
out.close();
}
} catch (IOException e) {
// Speichern ist nachrangig: Das Ticket wird dann nur nach einem Proxy-Neustart nicht automatisch gelöst
}
}
}
@@ -28,6 +28,8 @@ public final class Settings {
public final String messageShutdown;
public final String subjectTest;
public final String messageTest;
public final boolean resolveOnRecovery;
public final String messageRecovered;
public final int monitorIntervalSeconds;
public final int monitorFailures;
@@ -65,6 +67,9 @@ public final class Settings {
this.subjectTest = c.getString("ticket.test.subject", "[Minecraft] Testticket von {server}");
this.messageTest = c.getString("ticket.test.message", "Das ist ein Testticket des DeskHub-Minecraft-Plugins. Die Verbindung zu DeskHub funktioniert.\n\nServer: {server}\nZeit: {time}");
this.resolveOnRecovery = c.getBoolean("ticket.resolve-on-recovery", true);
this.messageRecovered = c.getString("ticket.recovered.message", "Der Minecraft-Server \"{server}\" ist seit {time} wieder erreichbar (Ausfall: {downtime}). Das Ticket wurde automatisch gelöst.");
this.monitorIntervalSeconds = clamp(c.getInt("monitor.interval-seconds", 30), 5, 3600);
this.monitorFailures = clamp(c.getInt("monitor.failures-before-ticket", 3), 1, 100);
this.cooldownMinutes = clamp(c.getInt("monitor.cooldown-minutes", 30), 0, 10080);
@@ -8,7 +8,7 @@ import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
/** Legt über die öffentliche Ticket-API von DeskHub (POST /api/public/v1/tickets.php) ein Ticket an. Blockierend: nie im Haupt-Thread aufrufen. */
/** Legt über die öffentliche Ticket-API von DeskHub (POST /api/public/v1/tickets.php) ein Ticket an oder löst es wieder. Blockierend: nie im Haupt-Thread aufrufen. */
public final class TicketClient {
private static final int MAX_RESPONSE = 65536;
private final Settings settings;
@@ -32,7 +32,21 @@ public final class TicketClient {
return b.append('}').toString();
}
/** Baut den JSON-Körper für "Ticket lösen" (öffentlich für Tests). */
public String buildResolveBody(String number, String message) {
return "{\"action\":\"resolve\",\"number\":" + Json.quote(number) + ",\"email\":" + Json.quote(settings.requesterEmail) + ",\"message\":" + Json.quote(clip(message, 2000)) + "}";
}
public TicketResult create(String subject, String message) {
return send(buildBody(subject, message));
}
/** Setzt ein vom Plugin angelegtes Ticket auf "Gelöst" (Server wieder erreichbar). */
public TicketResult resolve(String number, String message) {
return send(buildResolveBody(number, message));
}
private TicketResult send(String json) {
if (!settings.problems().isEmpty()) {
return TicketResult.failure(0, "Einstellungen unvollständig: " + settings.problems().get(0));
}
@@ -49,7 +63,7 @@ public final class TicketClient {
c.setRequestProperty("Accept", "application/json");
c.setRequestProperty("X-API-Key", settings.apiKey);
c.setRequestProperty("User-Agent", userAgent);
byte[] data = buildBody(subject, message).getBytes(StandardCharsets.UTF_8);
byte[] data = json.getBytes(StandardCharsets.UTF_8);
c.setFixedLengthStreamingMode(data.length);
OutputStream out = c.getOutputStream();
try {
@@ -47,6 +47,11 @@ ticket:
Ausfalldauer: ca. {downtime}
Bitte die Logdateien prüfen.
# Ist der Server wieder erreichbar (BungeeCord) bzw. neu gestartet (Spigot/Paper), wird das Ticket automatisch auf "Gelöst" gesetzt und bekommt diese Notiz.
# Ausschalten mit false (dann bleibt das Ticket offen, bis ihr es selbst löst).
resolve-on-recovery: true
recovered:
message: 'Der Minecraft-Server "{server}" ist seit {time} wieder erreichbar (Ausfall: {downtime}). Das Ticket wurde automatisch gelöst.'
shutdown:
subject: "[Minecraft] {server} wurde heruntergefahren"
message: 'Der Minecraft-Server "{server}" wurde um {time} heruntergefahren.'