2013-04-13 09:06:23 +02:00
|
|
|
From 2543e5197437caa9f4b741205fda322e7446ad60 Mon Sep 17 00:00:00 2001
|
2013-02-23 02:37:58 +01:00
|
|
|
From: md_5 <md_5@live.com.au>
|
|
|
|
Date: Sat, 23 Feb 2013 12:33:20 +1100
|
|
|
|
Subject: [PATCH] Watchdog Thread.
|
|
|
|
|
|
|
|
|
|
|
|
diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java
|
2013-04-13 09:06:23 +02:00
|
|
|
index 0205822..65a4e9f 100644
|
2013-02-23 02:37:58 +01:00
|
|
|
--- a/src/main/java/net/minecraft/server/MinecraftServer.java
|
|
|
|
+++ b/src/main/java/net/minecraft/server/MinecraftServer.java
|
2013-04-10 04:36:11 +02:00
|
|
|
@@ -403,6 +403,7 @@ public abstract class MinecraftServer implements ICommandListener, Runnable, IMo
|
2013-02-23 02:37:58 +01:00
|
|
|
this.q();
|
2013-02-26 18:21:40 +01:00
|
|
|
SpigotTimings.serverTickTimer.stopTiming();
|
|
|
|
org.bukkit.CustomTimingsHandler.tick();
|
2013-02-23 02:37:58 +01:00
|
|
|
+ org.spigotmc.WatchdogThread.tick();
|
|
|
|
}
|
|
|
|
// Spigot end
|
|
|
|
} else {
|
2013-04-10 04:36:11 +02:00
|
|
|
@@ -430,6 +431,7 @@ public abstract class MinecraftServer implements ICommandListener, Runnable, IMo
|
2013-02-23 02:37:58 +01:00
|
|
|
this.a(crashreport);
|
|
|
|
} finally {
|
|
|
|
try {
|
|
|
|
+ org.spigotmc.WatchdogThread.doStop();
|
|
|
|
this.stop();
|
|
|
|
this.isStopped = true;
|
|
|
|
} catch (Throwable throwable1) {
|
|
|
|
diff --git a/src/main/java/org/bukkit/craftbukkit/Spigot.java b/src/main/java/org/bukkit/craftbukkit/Spigot.java
|
2013-03-28 08:38:42 +01:00
|
|
|
index b00c885..ac99395 100644
|
2013-02-23 02:37:58 +01:00
|
|
|
--- a/src/main/java/org/bukkit/craftbukkit/Spigot.java
|
|
|
|
+++ b/src/main/java/org/bukkit/craftbukkit/Spigot.java
|
|
|
|
@@ -1,5 +1,6 @@
|
|
|
|
package org.bukkit.craftbukkit;
|
|
|
|
|
|
|
|
+import java.io.File;
|
|
|
|
import java.io.IOException;
|
|
|
|
import java.util.ArrayList;
|
|
|
|
import net.minecraft.server.*;
|
2013-03-23 08:05:22 +01:00
|
|
|
@@ -8,21 +9,24 @@ import org.bukkit.configuration.file.YamlConfiguration;
|
2013-02-26 18:21:40 +01:00
|
|
|
|
2013-02-23 02:37:58 +01:00
|
|
|
import java.util.List;
|
|
|
|
import java.util.logging.Level;
|
|
|
|
-import java.util.logging.Logger;
|
2013-03-23 08:05:22 +01:00
|
|
|
+import net.minecraft.server.EntityPlayer;
|
2013-02-23 02:37:58 +01:00
|
|
|
import org.bukkit.Bukkit;
|
|
|
|
import org.spigotmc.Metrics;
|
|
|
|
+import org.spigotmc.RestartCommand;
|
|
|
|
+import org.spigotmc.WatchdogThread;
|
|
|
|
|
|
|
|
public class Spigot {
|
2013-03-23 08:05:22 +01:00
|
|
|
- static AxisAlignedBB maxBB = AxisAlignedBB.a(0,0,0,0,0,0);
|
|
|
|
- static AxisAlignedBB miscBB = AxisAlignedBB.a(0,0,0,0,0,0);
|
|
|
|
- static AxisAlignedBB animalBB = AxisAlignedBB.a(0,0,0,0,0,0);
|
|
|
|
- static AxisAlignedBB monsterBB = AxisAlignedBB.a(0,0,0,0,0,0);
|
|
|
|
|
|
|
|
+ static AxisAlignedBB maxBB = AxisAlignedBB.a(0, 0, 0, 0, 0, 0);
|
|
|
|
+ static AxisAlignedBB miscBB = AxisAlignedBB.a(0, 0, 0, 0, 0, 0);
|
|
|
|
+ static AxisAlignedBB animalBB = AxisAlignedBB.a(0, 0, 0, 0, 0, 0);
|
|
|
|
+ static AxisAlignedBB monsterBB = AxisAlignedBB.a(0, 0, 0, 0, 0, 0);
|
|
|
|
public static boolean tabPing = false;
|
|
|
|
private static Metrics metrics;
|
2013-02-23 02:37:58 +01:00
|
|
|
|
|
|
|
public static void initialize(CraftServer server, SimpleCommandMap commandMap, YamlConfiguration configuration) {
|
|
|
|
commandMap.register("bukkit", new org.bukkit.craftbukkit.command.TicksPerSecondCommand("tps"));
|
|
|
|
+ commandMap.register("restart", new RestartCommand("restart"));
|
|
|
|
|
|
|
|
server.whitelistMessage = configuration.getString("settings.whitelist-message", server.whitelistMessage);
|
|
|
|
server.stopMessage = configuration.getString("settings.stop-message", server.stopMessage);
|
2013-03-23 00:58:55 +01:00
|
|
|
@@ -31,13 +35,24 @@ public class Spigot {
|
2013-02-24 03:37:20 +01:00
|
|
|
server.commandComplete = configuration.getBoolean("settings.command-complete", true);
|
2013-02-23 02:37:58 +01:00
|
|
|
server.spamGuardExclusions = configuration.getStringList("settings.spam-exclusions");
|
|
|
|
|
|
|
|
+ int configVersion = configuration.getInt("config-version");
|
|
|
|
+ switch (configVersion) {
|
|
|
|
+ case 0:
|
|
|
|
+ configuration.set("settings.timeout-time", 30);
|
2013-02-27 02:04:04 +01:00
|
|
|
+ case 1:
|
|
|
|
+ configuration.set("settings.timeout-time", 60);
|
2013-02-23 02:37:58 +01:00
|
|
|
+ }
|
2013-02-27 02:04:04 +01:00
|
|
|
+ configuration.set("config-version", 2);
|
2013-02-23 02:37:58 +01:00
|
|
|
+
|
2013-02-27 02:04:04 +01:00
|
|
|
+ WatchdogThread.doStart(configuration.getInt("settings.timeout-time", 60), configuration.getBoolean("settings.restart-on-crash", false));
|
2013-02-23 02:37:58 +01:00
|
|
|
+
|
|
|
|
server.orebfuscatorEnabled = configuration.getBoolean("orebfuscator.enable", false);
|
|
|
|
server.orebfuscatorEngineMode = configuration.getInt("orebfuscator.engine-mode", 1);
|
|
|
|
server.orebfuscatorUpdateRadius = configuration.getInt("orebfuscator.update-radius", 2);
|
|
|
|
server.orebfuscatorDisabledWorlds = configuration.getStringList("orebfuscator.disabled-worlds");
|
2013-03-23 00:58:55 +01:00
|
|
|
server.orebfuscatorBlocks = configuration.getShortList("orebfuscator.blocks");
|
2013-02-23 02:37:58 +01:00
|
|
|
if (server.orebfuscatorEngineMode != 1 && server.orebfuscatorEngineMode != 2) {
|
|
|
|
- server.orebfuscatorEngineMode = 1;
|
|
|
|
+ server.orebfuscatorEngineMode = 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (server.chunkGCPeriod == 0) {
|
2013-03-23 08:05:22 +01:00
|
|
|
@@ -59,6 +74,7 @@ public class Spigot {
|
|
|
|
/**
|
|
|
|
* Initializes an entities type on construction to specify what group this
|
|
|
|
* entity is in for activation ranges.
|
|
|
|
+ *
|
|
|
|
* @param entity
|
|
|
|
* @return group id
|
|
|
|
*/
|
|
|
|
@@ -80,21 +96,20 @@ public class Spigot {
|
|
|
|
* @return boolean If it should always tick.
|
|
|
|
*/
|
|
|
|
public static boolean initializeEntityActivationState(Entity entity, CraftWorld world) {
|
|
|
|
- if ( (entity.activationType == 3 && world.miscEntityActivationRange == 0)
|
|
|
|
- || (entity.activationType == 2 && world.animalEntityActivationRange == 0)
|
|
|
|
- || (entity.activationType == 1 && world.monsterEntityActivationRange == 0)
|
|
|
|
- || entity instanceof EntityHuman
|
|
|
|
- || entity instanceof EntityItemFrame
|
|
|
|
- || entity instanceof EntityProjectile
|
|
|
|
- || entity instanceof EntityEnderDragon
|
|
|
|
- || entity instanceof EntityComplexPart
|
|
|
|
- || entity instanceof EntityWither
|
|
|
|
- || entity instanceof EntityFireball
|
|
|
|
- || entity instanceof EntityWeather
|
|
|
|
- || entity instanceof EntityTNTPrimed
|
|
|
|
- || entity instanceof EntityEnderCrystal
|
|
|
|
- || entity instanceof EntityFireworks
|
|
|
|
- ) {
|
|
|
|
+ if ((entity.activationType == 3 && world.miscEntityActivationRange == 0)
|
|
|
|
+ || (entity.activationType == 2 && world.animalEntityActivationRange == 0)
|
|
|
|
+ || (entity.activationType == 1 && world.monsterEntityActivationRange == 0)
|
|
|
|
+ || entity instanceof EntityHuman
|
|
|
|
+ || entity instanceof EntityItemFrame
|
|
|
|
+ || entity instanceof EntityProjectile
|
|
|
|
+ || entity instanceof EntityEnderDragon
|
|
|
|
+ || entity instanceof EntityComplexPart
|
|
|
|
+ || entity instanceof EntityWither
|
|
|
|
+ || entity instanceof EntityFireball
|
|
|
|
+ || entity instanceof EntityWeather
|
|
|
|
+ || entity instanceof EntityTNTPrimed
|
|
|
|
+ || entity instanceof EntityEnderCrystal
|
|
|
|
+ || entity instanceof EntityFireworks) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
@@ -104,6 +119,7 @@ public class Spigot {
|
|
|
|
/**
|
|
|
|
* Utility method to grow an AABB without creating a new AABB or touching
|
|
|
|
* the pool, so we can re-use ones we have.
|
|
|
|
+ *
|
|
|
|
* @param target
|
|
|
|
* @param source
|
|
|
|
* @param x
|
|
|
|
@@ -122,6 +138,7 @@ public class Spigot {
|
|
|
|
/**
|
|
|
|
* Find what entities are in range of the players in the world and set
|
|
|
|
* active if in range.
|
|
|
|
+ *
|
|
|
|
* @param world
|
|
|
|
*/
|
|
|
|
public static void activateEntities(World world) {
|
|
|
|
@@ -160,6 +177,7 @@ public class Spigot {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks for the activation state of all entities in this chunk.
|
|
|
|
+ *
|
|
|
|
* @param chunk
|
|
|
|
*/
|
|
|
|
private static void activateChunkEntities(Chunk chunk) {
|
|
|
|
@@ -195,6 +213,7 @@ public class Spigot {
|
|
|
|
/**
|
|
|
|
* If an entity is not in range, do some more checks to see if we should
|
|
|
|
* give it a shot.
|
|
|
|
+ *
|
|
|
|
* @param entity
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
@@ -205,7 +224,7 @@ public class Spigot {
|
|
|
|
}
|
|
|
|
if (!(entity instanceof EntityArrow)) {
|
|
|
|
if (!entity.onGround || entity.passenger != null
|
|
|
|
- || entity.vehicle != null) {
|
|
|
|
+ || entity.vehicle != null) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
} else if (!((EntityArrow) entity).inGround) {
|
|
|
|
@@ -235,6 +254,7 @@ public class Spigot {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if the entity is active for this tick.
|
|
|
|
+ *
|
|
|
|
* @param entity
|
|
|
|
* @return
|
|
|
|
*/
|
2013-03-28 08:38:42 +01:00
|
|
|
@@ -265,4 +285,64 @@ public class Spigot {
|
2013-02-26 18:21:40 +01:00
|
|
|
SpigotTimings.checkIfActiveTimer.stopTiming();
|
|
|
|
return isActive;
|
2013-02-23 02:37:58 +01:00
|
|
|
}
|
|
|
|
+
|
|
|
|
+ public static void restart() {
|
|
|
|
+ try {
|
|
|
|
+ String startupScript = MinecraftServer.getServer().server.configuration.getString("settings.restart-script-location", "");
|
2013-02-24 01:30:30 +01:00
|
|
|
+ final File file = new File(startupScript);
|
2013-02-23 02:37:58 +01:00
|
|
|
+ if (file.isFile()) {
|
|
|
|
+ System.out.println("Attempting to restart with " + startupScript);
|
|
|
|
+
|
|
|
|
+ // Kick all players
|
2013-03-23 08:05:22 +01:00
|
|
|
+ for (EntityPlayer p : (List< EntityPlayer>) MinecraftServer.getServer().getPlayerList().players) {
|
2013-03-28 08:38:42 +01:00
|
|
|
+ p.playerConnection.networkManager.queue(new Packet255KickDisconnect("Server is restarting"));
|
|
|
|
+ p.playerConnection.networkManager.d();
|
2013-02-23 02:37:58 +01:00
|
|
|
+ }
|
|
|
|
+ // Give the socket a chance to send the packets
|
|
|
|
+ try {
|
|
|
|
+ Thread.sleep(100);
|
|
|
|
+ } catch (InterruptedException ex) {
|
|
|
|
+ }
|
|
|
|
+ // Close the socket so we can rebind with the new process
|
|
|
|
+ MinecraftServer.getServer().ae().a();
|
|
|
|
+
|
|
|
|
+ // Give time for it to kick in
|
|
|
|
+ try {
|
|
|
|
+ Thread.sleep(100);
|
|
|
|
+ } catch (InterruptedException ex) {
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // Actually shutdown
|
|
|
|
+ try {
|
|
|
|
+ MinecraftServer.getServer().stop();
|
|
|
|
+ } catch (Throwable t) {
|
|
|
|
+ }
|
|
|
|
+
|
2013-02-24 01:30:30 +01:00
|
|
|
+ // This will be done AFTER the server has completely halted
|
|
|
|
+ Thread shutdownHook = new Thread() {
|
|
|
|
+ @Override
|
2013-03-23 08:05:22 +01:00
|
|
|
+ public void run() {
|
2013-02-24 01:30:30 +01:00
|
|
|
+ try {
|
|
|
|
+ String os = System.getProperty("os.name").toLowerCase();
|
|
|
|
+ if (os.contains("win")) {
|
|
|
|
+ Runtime.getRuntime().exec("cmd /c start " + file.getPath());
|
|
|
|
+ } else {
|
2013-03-23 08:05:22 +01:00
|
|
|
+ Runtime.getRuntime().exec(new String[]{"sh", file.getPath()});
|
2013-02-24 01:30:30 +01:00
|
|
|
+ }
|
2013-03-23 08:05:22 +01:00
|
|
|
+ } catch (Exception e) {
|
2013-02-24 01:30:30 +01:00
|
|
|
+ e.printStackTrace();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ };
|
|
|
|
+
|
|
|
|
+ shutdownHook.setDaemon(true);
|
|
|
|
+ Runtime.getRuntime().addShutdownHook(shutdownHook);
|
2013-02-23 02:37:58 +01:00
|
|
|
+ } else {
|
2013-03-13 08:28:49 +01:00
|
|
|
+ System.out.println("Startup script '" + startupScript + "' does not exist! Stopping server.");
|
2013-02-23 02:37:58 +01:00
|
|
|
+ }
|
2013-03-13 08:28:49 +01:00
|
|
|
+ System.exit(0);
|
2013-02-23 02:37:58 +01:00
|
|
|
+ } catch (Exception ex) {
|
|
|
|
+ ex.printStackTrace();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
}
|
|
|
|
diff --git a/src/main/java/org/spigotmc/RestartCommand.java b/src/main/java/org/spigotmc/RestartCommand.java
|
|
|
|
new file mode 100644
|
|
|
|
index 0000000..2d5c89f
|
|
|
|
--- /dev/null
|
|
|
|
+++ b/src/main/java/org/spigotmc/RestartCommand.java
|
|
|
|
@@ -0,0 +1,23 @@
|
|
|
|
+package org.spigotmc;
|
|
|
|
+
|
|
|
|
+import org.bukkit.command.Command;
|
|
|
|
+import org.bukkit.command.CommandSender;
|
|
|
|
+import org.bukkit.craftbukkit.Spigot;
|
|
|
|
+
|
|
|
|
+public class RestartCommand extends Command {
|
|
|
|
+
|
|
|
|
+ public RestartCommand(String name) {
|
|
|
|
+ super(name);
|
|
|
|
+ this.description = "Restarts the server";
|
|
|
|
+ this.usageMessage = "/restart";
|
|
|
|
+ this.setPermission("bukkit.command.restart");
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public boolean execute(CommandSender sender, String currentAlias, String[] args) {
|
|
|
|
+ if (testPermission(sender)) {
|
|
|
|
+ Spigot.restart();
|
|
|
|
+ }
|
|
|
|
+ return true;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
diff --git a/src/main/java/org/spigotmc/WatchdogThread.java b/src/main/java/org/spigotmc/WatchdogThread.java
|
|
|
|
new file mode 100644
|
|
|
|
index 0000000..10390b8
|
|
|
|
--- /dev/null
|
|
|
|
+++ b/src/main/java/org/spigotmc/WatchdogThread.java
|
|
|
|
@@ -0,0 +1,93 @@
|
|
|
|
+package org.spigotmc;
|
|
|
|
+
|
|
|
|
+import java.lang.management.ManagementFactory;
|
|
|
|
+import java.lang.management.MonitorInfo;
|
|
|
|
+import java.lang.management.ThreadInfo;
|
|
|
|
+import java.util.logging.Level;
|
|
|
|
+import java.util.logging.Logger;
|
|
|
|
+import org.bukkit.Bukkit;
|
|
|
|
+import org.bukkit.craftbukkit.Spigot;
|
|
|
|
+
|
|
|
|
+public class WatchdogThread extends Thread {
|
|
|
|
+
|
|
|
|
+ private static WatchdogThread instance;
|
|
|
|
+ private final long timeoutTime;
|
|
|
|
+ private final boolean restart;
|
|
|
|
+ private volatile long lastTick;
|
|
|
|
+ private volatile boolean stopping;
|
|
|
|
+
|
|
|
|
+ private WatchdogThread(long timeoutTime, boolean restart) {
|
|
|
|
+ super("Spigot Watchdog Thread");
|
|
|
|
+ this.timeoutTime = timeoutTime;
|
|
|
|
+ this.restart = restart;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public static void doStart(int timeoutTime, boolean restart) {
|
|
|
|
+ if (instance == null) {
|
|
|
|
+ instance = new WatchdogThread(timeoutTime * 1000L, restart);
|
|
|
|
+ instance.start();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public static void tick() {
|
|
|
|
+ instance.lastTick = System.currentTimeMillis();
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public static void doStop() {
|
|
|
|
+ if (instance != null) {
|
|
|
|
+ instance.stopping = true;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ @Override
|
|
|
|
+ public void run() {
|
|
|
|
+ while (!stopping) {
|
|
|
|
+ //
|
|
|
|
+ if (lastTick != 0 && System.currentTimeMillis() > lastTick + timeoutTime) {
|
|
|
|
+ Logger log = Bukkit.getServer().getLogger();
|
|
|
|
+ log.log(Level.SEVERE, "The server has stopped responding!");
|
|
|
|
+ log.log(Level.SEVERE, "Please report this to http://www.spigotmc.org/");
|
|
|
|
+ log.log(Level.SEVERE, "Be sure to include ALL relevant console errors and Minecraft crash reports");
|
|
|
|
+ log.log(Level.SEVERE, "Spigot version: " + Bukkit.getServer().getVersion());
|
|
|
|
+ //
|
|
|
|
+ log.log(Level.SEVERE, "Current Thread State:");
|
|
|
|
+ ThreadInfo[] threads = ManagementFactory.getThreadMXBean().dumpAllThreads(true, true);
|
|
|
|
+ for (ThreadInfo thread : threads) {
|
|
|
|
+ if (thread.getThreadState() != State.WAITING) {
|
|
|
|
+ log.log(Level.SEVERE, "------------------------------");
|
|
|
|
+ //
|
|
|
|
+ log.log(Level.SEVERE, "Current Thread: " + thread.getThreadName());
|
|
|
|
+ log.log(Level.SEVERE, "\tPID: " + thread.getThreadId()
|
|
|
|
+ + " | Suspended: " + thread.isSuspended()
|
|
|
|
+ + " | Native: " + thread.isInNative()
|
|
|
|
+ + " | State: " + thread.getThreadState());
|
|
|
|
+ if (thread.getLockedMonitors().length != 0) {
|
|
|
|
+ log.log(Level.SEVERE, "\tThread is waiting on monitor(s):");
|
|
|
|
+ for (MonitorInfo monitor : thread.getLockedMonitors()) {
|
|
|
|
+ log.log(Level.SEVERE, "\t\tLocked on:" + monitor.getLockedStackFrame());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ log.log(Level.SEVERE, "\tStack:");
|
|
|
|
+ //
|
|
|
|
+ StackTraceElement[] stack = thread.getStackTrace();
|
|
|
|
+ for (int line = 0; line < stack.length; line++) {
|
|
|
|
+ log.log(Level.SEVERE, "\t\t" + stack[line].toString());
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ log.log(Level.SEVERE, "------------------------------");
|
|
|
|
+
|
|
|
|
+ if (restart) {
|
|
|
|
+ Spigot.restart();
|
|
|
|
+ }
|
|
|
|
+ break;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ try {
|
|
|
|
+ sleep(10000);
|
|
|
|
+ } catch (InterruptedException ex) {
|
|
|
|
+ interrupt();
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
2013-02-26 01:19:08 +01:00
|
|
|
diff --git a/src/main/resources/configurations/bukkit.yml b/src/main/resources/configurations/bukkit.yml
|
2013-03-25 08:57:00 +01:00
|
|
|
index 5822e41..a62ba24 100644
|
2013-02-26 01:19:08 +01:00
|
|
|
--- a/src/main/resources/configurations/bukkit.yml
|
|
|
|
+++ b/src/main/resources/configurations/bukkit.yml
|
|
|
|
@@ -31,6 +31,9 @@ settings:
|
|
|
|
command-complete: true
|
|
|
|
spam-exclusions:
|
|
|
|
- /skill
|
|
|
|
+ timeout-time: 30
|
|
|
|
+ restart-on-crash: false
|
|
|
|
+ restart-script-location: /path/to/server/start.sh
|
|
|
|
world-settings:
|
|
|
|
default:
|
|
|
|
growth-chunks-per-tick: 650
|
2013-02-23 02:37:58 +01:00
|
|
|
--
|
2013-04-10 04:36:11 +02:00
|
|
|
1.8.2.1
|
2013-02-23 02:37:58 +01:00
|
|
|
|