// $Id$
/*
* CommandHelper
* Copyright (C) 2010 sk89q <http://www.sk89q.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.laytonsmith.commandhelper;
import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscovery;
import com.laytonsmith.PureUtilities.ClassLoading.ClassDiscoveryCache;
import com.laytonsmith.PureUtilities.Common.FileUtil;
import com.laytonsmith.PureUtilities.Common.OSUtils;
import com.laytonsmith.PureUtilities.Common.ReflectionUtils;
import com.laytonsmith.PureUtilities.Common.StreamUtils;
import com.laytonsmith.PureUtilities.Common.StringUtils;
import com.laytonsmith.PureUtilities.ExecutionQueue;
import com.laytonsmith.PureUtilities.SimpleVersion;
import com.laytonsmith.PureUtilities.TermColors;
import com.laytonsmith.abstraction.Implementation;
import com.laytonsmith.abstraction.MCCommand;
import com.laytonsmith.abstraction.MCCommandSender;
import com.laytonsmith.abstraction.MCPlayer;
import com.laytonsmith.abstraction.MCServer;
import com.laytonsmith.abstraction.StaticLayer;
import com.laytonsmith.abstraction.bukkit.BukkitConvertor;
import com.laytonsmith.abstraction.bukkit.BukkitMCBlockCommandSender;
import com.laytonsmith.abstraction.bukkit.BukkitMCCommand;
import com.laytonsmith.abstraction.bukkit.entities.BukkitMCPlayer;
import com.laytonsmith.abstraction.enums.MCChatColor;
import com.laytonsmith.abstraction.enums.bukkit.BukkitMCBiomeType;
import com.laytonsmith.abstraction.enums.bukkit.BukkitMCEntityType;
import com.laytonsmith.abstraction.enums.bukkit.BukkitMCSound;
import com.laytonsmith.annotations.EventIdentifier;
import com.laytonsmith.core.AliasCore;
import com.laytonsmith.core.CHLog;
import com.laytonsmith.core.Installer;
import com.laytonsmith.core.Main;
import com.laytonsmith.core.MethodScriptExecutionQueue;
import com.laytonsmith.core.MethodScriptFileLocations;
import com.laytonsmith.core.Prefs;
import com.laytonsmith.core.Profiles;
import com.laytonsmith.core.Static;
import com.laytonsmith.core.UpgradeLog;
import com.laytonsmith.core.constructs.Target;
import com.laytonsmith.core.extensions.ExtensionManager;
import com.laytonsmith.core.profiler.Profiler;
import com.laytonsmith.persistence.PersistenceNetwork;
import org.bukkit.Server;
import org.bukkit.command.BlockCommandSender;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.command.ConsoleCommandSender;
import org.bukkit.entity.Player;
import org.bukkit.event.Event;
import org.bukkit.event.EventException;
import org.bukkit.event.EventHandler;
import org.bukkit.event.EventPriority;
import org.bukkit.event.HandlerList;
import org.bukkit.event.Listener;
import org.bukkit.event.player.PlayerCommandPreprocessEvent;
import org.bukkit.event.server.ServerCommandEvent;
import org.bukkit.plugin.EventExecutor;
import org.bukkit.plugin.RegisteredListener;
import org.bukkit.plugin.TimedRegisteredListener;
import org.bukkit.plugin.java.JavaPlugin;
import org.mcstats.Metrics;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* Entry point for the plugin.
*
* @author sk89q
*/
public class CommandHelperPlugin extends JavaPlugin {
//Do not rename this field, it is changed reflectively in unit tests.
private static AliasCore ac;
public static MCServer myServer;
public static SimpleVersion version;
public static CommandHelperPlugin self;
public static ExecutorService hostnameLookupThreadPool;
public static ConcurrentHashMap<String, String> hostnameLookupCache;
private static int hostnameThreadPoolID = 0;
public Profiler profiler;
public final ExecutionQueue executionQueue = new MethodScriptExecutionQueue("CommandHelperExecutionQueue", "default");
public PersistenceNetwork persistenceNetwork;
public Profiles profiles;
//public boolean firstLoad = true;
public long interpreterUnlockedUntil = 0;
private Thread loadingThread;
/**
* Listener for the plugin system.
*/
final CommandHelperListener playerListener =
new CommandHelperListener(this);
/**
* Interpreter listener
*/
public final CommandHelperInterpreterListener interpreterListener =
new CommandHelperInterpreterListener(this);
/**
* Server Command Listener, for console commands
*/
final CommandHelperServerListener serverListener =
new CommandHelperServerListener();
@Override
public void onLoad() {
Implementation.setServerType(Implementation.Type.BUKKIT);
CommandHelperFileLocations.setDefault(new CommandHelperFileLocations());
CommandHelperFileLocations.getDefault().getCacheDirectory().mkdirs();
CommandHelperFileLocations.getDefault().getPreferencesDirectory().mkdirs();
UpgradeLog upgradeLog = new UpgradeLog(CommandHelperFileLocations.getDefault().getUpgradeLogFile());
upgradeLog.addUpgradeTask(new UpgradeLog.UpgradeTask() {
String version = null;
@Override
public boolean doRun() {
try {
version = "versionUpgrade-" + Main.loadSelfVersion();
return !hasBreadcrumb(version);
} catch (Exception ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
return false;
}
}
@Override
public void run() {
leaveBreadcrumb(version);
}
});
upgradeLog.addUpgradeTask(new UpgradeLog.UpgradeTask() {
File oldPreferences = new File(CommandHelperFileLocations.getDefault().getConfigDirectory(),
"preferences.txt");
@Override
public boolean doRun() {
return oldPreferences.exists()
&& !CommandHelperFileLocations.getDefault().getPreferencesFile().exists();
}
@Override
public void run() {
try {
Prefs.init(oldPreferences);
Prefs.SetColors();
Logger.getLogger("Minecraft").log(Level.INFO,
TermColors.YELLOW + "[" + Implementation.GetServerType().getBranding() + "] Old preferences.txt file detected. Moving preferences.txt to preferences.ini." + TermColors.reset());
FileUtil.copy(oldPreferences, CommandHelperFileLocations.getDefault().getPreferencesFile(), true);
oldPreferences.deleteOnExit();
} catch (IOException ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
}
}
});
upgradeLog.addUpgradeTask(new UpgradeLog.UpgradeTask() {
File cd = CommandHelperFileLocations.getDefault().getConfigDirectory();
private final String breadcrumb = "move-preference-files-v1.0";
@Override
public boolean doRun() {
return !hasBreadcrumb(breadcrumb)
&& new File(cd, "preferences.ini").exists();
}
@Override
public void run() {
//We need to move the following files:
//1. persistance.config to prefs/persistence.ini (note the correct spelling)
//2. preferences.ini to prefs/preferences.ini
//3. profiler.config to prefs/profiler.ini
//4. sql-profiles.xml to prefs/sql-profiles.xml
//5. We are not moving loggerPreferences.txt, instead just deleting it,
// because the defaults have changed. Most people aren't using this feature
// anyways. (The new one will write itself out upon installation.)
//Other than the config/prefs directory, we are hardcoding all the values, so
//we know they are correct (for old values). Any errors will be reported, but will not
//stop the entire process.
CommandHelperFileLocations p = CommandHelperFileLocations.getDefault();
try {
FileUtil.move(new File(cd, "persistance.config"), p.getPersistenceConfig());
} catch (IOException ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
}
try {
FileUtil.move(new File(cd, "preferences.ini"), p.getPreferencesFile());
} catch (IOException ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
}
try {
FileUtil.move(new File(cd, "profiler.config"), p.getProfilerConfigFile());
} catch (IOException ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
}
try {
FileUtil.move(new File(cd, "sql-profiles.xml"), p.getProfilesFile());
} catch (IOException ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
}
new File(cd, "logs/debug/loggerPreferences.txt").delete();
leaveBreadcrumb(breadcrumb);
StreamUtils.GetSystemOut().println("CommandHelper: Your preferences files have all been relocated to " + p.getPreferencesDirectory());
StreamUtils.GetSystemOut().println("CommandHelper: The loggerPreferences.txt file has been deleted and re-created, as the defaults have changed.");
}
});
// Renames the sql-profiles.xml file to the new name.
upgradeLog.addUpgradeTask(new UpgradeLog.UpgradeTask() {
// This should never change
private final File oldProfilesFile = new File(MethodScriptFileLocations.getDefault().getPreferencesDirectory(), "sql-profiles.xml");
@Override
public boolean doRun() {
return oldProfilesFile.exists();
}
@Override
public void run() {
try {
FileUtil.move(oldProfilesFile, MethodScriptFileLocations.getDefault().getProfilesFile());
StreamUtils.GetSystemOut().println("CommandHelper: sql-profiles.xml has been renamed to " + MethodScriptFileLocations.getDefault().getProfilesFile().getName());
} catch (IOException ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
}
}
});
try {
upgradeLog.runTasks();
} catch (IOException ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
}
try{
Prefs.init(CommandHelperFileLocations.getDefault().getPreferencesFile());
} catch (IOException ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
}
Prefs.SetColors();
CHLog.initialize(CommandHelperFileLocations.getDefault().getConfigDirectory());
Installer.Install(CommandHelperFileLocations.getDefault().getConfigDirectory());
if(new SimpleVersion(System.getProperty("java.version")).lt(new SimpleVersion("1.7"))){
CHLog.GetLogger().w(CHLog.Tags.GENERAL, "You appear to be running a version of Java older than Java 7. You should have plans"
+ " to upgrade at some point, as " + Implementation.GetServerType().getBranding() + " may require it at some point.", Target.UNKNOWN);
}
self = this;
ClassDiscoveryCache cdc = new ClassDiscoveryCache(CommandHelperFileLocations.getDefault().getCacheDirectory());
cdc.setLogger(Logger.getLogger(CommandHelperPlugin.class.getName()));
ClassDiscovery.getDefaultInstance().setClassDiscoveryCache(cdc);
ClassDiscovery.getDefaultInstance().addDiscoveryLocation(ClassDiscovery.GetClassContainer(CommandHelperPlugin.class));
ClassDiscovery.getDefaultInstance().addDiscoveryLocation(ClassDiscovery.GetClassContainer(Server.class));
StreamUtils.GetSystemOut().println("[CommandHelper] Running initial class discovery,"
+ " this will probably take a few seconds...");
myServer = StaticLayer.GetServer();
StreamUtils.GetSystemOut().println("[CommandHelper] Loading extensions in the background...");
loadingThread = new Thread("extensionloader") {
@Override
public void run() {
ExtensionManager.AddDiscoveryLocation(CommandHelperFileLocations.getDefault().getExtensionsDirectory());
if (OSUtils.GetOS() == OSUtils.OS.WINDOWS) {
// Using StreamUtils.GetSystemOut() here instead of the logger as the logger doesn't
// immediately print to the console.
StreamUtils.GetSystemOut().println("[CommandHelper] Caching extensions...");
ExtensionManager.Cache(CommandHelperFileLocations.getDefault().getExtensionCacheDirectory());
StreamUtils.GetSystemOut().println("[CommandHelper] Extension caching complete.");
}
ExtensionManager.Initialize(ClassDiscovery.getDefaultInstance());
StreamUtils.GetSystemOut().println("[CommandHelper] Extension loading complete.");
}
};
loadingThread.start();
}
/**
* Called on plugin enable.
*/
@Override
public void onEnable() {
if(loadingThread.isAlive()){
StreamUtils.GetSystemOut().println("[CommandHelper] Waiting for extension loading to complete...");
try {
loadingThread.join();
} catch (InterruptedException ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
}
}
BukkitMCEntityType.build();
BukkitMCBiomeType.build();
BukkitMCSound.build();
//Metrics
try {
Metrics m = new Metrics(this);
Metrics.Graph graph = m.createGraph("Player count");
graph.addPlotter(new Metrics.Plotter("Player count") {
@Override
public int getValue() {
return Static.getServer().getOnlinePlayers().size();
}
});
m.addGraph(graph);
m.start();
} catch (IOException e) {
// Failed to submit the stats :-(
}
try {
//This may seem redundant, but on a /reload, we want to refresh these
//properties.
Prefs.init(CommandHelperFileLocations.getDefault().getPreferencesFile());
} catch (IOException ex) {
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.SEVERE, null, ex);
}
if(Prefs.UseSudoFallback()){
Logger.getLogger(CommandHelperPlugin.class.getName()).log(Level.WARNING, "In your preferences, use-sudo-fallback is turned on. Consider turning this off if you can.");
}
CHLog.initialize(CommandHelperFileLocations.getDefault().getConfigDirectory());
version = new SimpleVersion(getDescription().getVersion());
String script_name = Prefs.ScriptName();
String main_file = Prefs.MainFile();
boolean showSplashScreen = Prefs.ShowSplashScreen();
if (showSplashScreen) {
StreamUtils.GetSystemOut().println(TermColors.reset());
//StreamUtils.GetSystemOut().flush();
StreamUtils.GetSystemOut().println("\n\n\n" + Static.Logo());
}
ac = new AliasCore(new File(CommandHelperFileLocations.getDefault().getConfigDirectory(), script_name),
CommandHelperFileLocations.getDefault().getLocalPackagesDirectory(),
CommandHelperFileLocations.getDefault().getPreferencesFile(),
new File(CommandHelperFileLocations.getDefault().getConfigDirectory(), main_file), this);
ac.reload(null, null, true);
//Clear out our hostname cache
hostnameLookupCache = new ConcurrentHashMap<>();
//Create a new thread pool, with a custom ThreadFactory,
//so we can more clearly name our threads.
hostnameLookupThreadPool = Executors.newFixedThreadPool(3, new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "CommandHelperHostnameLookup-" + (++hostnameThreadPoolID));
}
});
for (Player p : getServer().getOnlinePlayers()) {
//Repopulate our cache for currently online players.
//New players that join later will get a lookup done
//on them at that time.
Static.HostnameCache(p.getName(), p.getAddress());
}
BukkitDirtyRegisteredListener.PlayDirty();
registerEvents(playerListener);
//interpreter events
registerEvents(interpreterListener);
registerEvents(serverListener);
//Script events
StaticLayer.Startup(this);
playerListener.loadGlobalAliases();
interpreterListener.reload();
Static.getLogger().log(Level.INFO, "[CommandHelper] CommandHelper {0} enabled", getDescription().getVersion());
}
public static AliasCore getCore() {
return ac;
}
/**
* Disables the plugin.
*/
@Override
public void onDisable() {
//free up some memory
StaticLayer.GetConvertor().runShutdownHooks();
stopExecutionQueue();
ExtensionManager.Cleanup();
ac = null;
}
public void stopExecutionQueue() {
for (String queue : executionQueue.activeQueues()) {
executionQueue.clear(queue);
}
}
/**
* Register all events in a Listener class.
*
* @param listener
*/
public void registerEvents(Listener listener) {
getServer().getPluginManager().registerEvents(listener, this);
}
/*
* This method is based on Bukkit's JavaPluginLoader:createRegisteredListeners
* Part of this code would be run normally using the other register method
*/
public void registerEventsDynamic(Listener listener) {
for (final java.lang.reflect.Method method : listener.getClass().getMethods()) {
EventIdentifier identifier = method.getAnnotation(EventIdentifier.class);
EventHandler defaultHandler = method.getAnnotation(EventHandler.class);
EventPriority priority = EventPriority.LOWEST;
Class<? extends Event> eventClass;
if (defaultHandler != null) {
priority = defaultHandler.priority();
}
if (identifier == null) {
if (defaultHandler != null && method.getParameterTypes().length == 1) {
try {
eventClass = (Class<? extends Event>) method.getParameterTypes()[0];
} catch (ClassCastException e) {
continue;
}
} else {
continue;
}
} else {
if (!identifier.event().existsInCurrent()) {
continue;
}
try {
eventClass = (Class<? extends Event>) Class.forName(identifier.className());
} catch (ClassNotFoundException | ClassCastException e) {
CHLog.GetLogger().e(CHLog.Tags.RUNTIME, "Could not listen for " + identifier.event().name()
+ " because the class " + identifier.className() + " could not be found."
+ " This problem is not expected to occur, so please report it on the bug"
+ " tracker if it does.", Target.UNKNOWN);
continue;
}
}
HandlerList handler;
try {
handler = (HandlerList) ReflectionUtils.invokeMethod(eventClass, null, "getHandlerList");
} catch (ReflectionUtils.ReflectionException ref) {
Class eventSuperClass = eventClass.getSuperclass();
if (eventSuperClass != null) {
try {
handler = (HandlerList) ReflectionUtils.invokeMethod(eventSuperClass, null, "getHandlerList");
} catch (ReflectionUtils.ReflectionException refInner) {
CHLog.GetLogger().e(CHLog.Tags.RUNTIME, "Could not listen for " + identifier.event().name()
+ " because the handler for class " + identifier.className()
+ " could not be found. An attempt has already been made to find the"
+ " correct handler, but" + eventSuperClass.getName()
+ " did not have it either. Please report this on the bug tracker.",
Target.UNKNOWN);
continue;
}
} else {
CHLog.GetLogger().e(CHLog.Tags.RUNTIME, "Could not listen for " + identifier.event().name()
+ " because the handler for class " + identifier.className()
+ " could not be found. An attempt has already been made to find the"
+ " correct handler, but no superclass could be found."
+ " Please report this on the bug tracker.",
Target.UNKNOWN);
continue;
}
}
final Class<? extends Event> finalEventClass = eventClass;
EventExecutor executor = new EventExecutor() {
@Override
public void execute(Listener listener, Event event) throws EventException {
try {
if (!finalEventClass.isAssignableFrom(event.getClass())) {
return;
}
method.invoke(listener, event);
} catch (InvocationTargetException ex) {
throw new EventException(ex.getCause());
} catch (Throwable t) {
throw new EventException(t);
}
}
};
if (this.getServer().getPluginManager().useTimings()) {
handler.register(new TimedRegisteredListener(listener, executor, priority, this, false));
} else {
handler.register(new RegisteredListener(listener, executor, priority, this, false));
}
}
}
@Override
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
MCCommandSender mcsender = BukkitConvertor.BukkitGetCorrectSender(sender);
MCCommand cmd = new BukkitMCCommand(command);
return cmd.handleTabComplete(mcsender, alias, args);
}
/**
* Called when a command registered by this plugin is received.
* @param sender
* @param cmd
* @param commandLabel
* @param args
* @return
*/
@Override
public boolean onCommand(CommandSender sender, Command cmd, String commandLabel, String[] args) {
String cmdName = cmd.getName().toLowerCase();
if ((sender.isOp() || (sender instanceof Player && (sender.hasPermission("commandhelper.reloadaliases")
|| sender.hasPermission("ch.reloadaliases"))))
&& (cmdName.equals("reloadaliases") || cmdName.equals("reloadalias") || cmdName.equals("recompile"))) {
MCPlayer player = null;
if (sender instanceof Player) {
player = new BukkitMCPlayer((Player) sender);
}
ac.reload(player, args, false);
return true;
} else if (cmdName.equalsIgnoreCase("commandhelper")) {
return args.length >= 1 && args[0].equalsIgnoreCase("null");
} else if (cmdName.equals("runalias")) {
//Hardcoded alias rebroadcast
if(args.length == 0){
return false;
}
String command = StringUtils.Join(args, " ");
if (sender instanceof Player) {
PlayerCommandPreprocessEvent pcpe = new PlayerCommandPreprocessEvent((Player) sender, command);
playerListener.onPlayerCommandPreprocess(pcpe);
} else if (sender instanceof ConsoleCommandSender) {
if (command.startsWith("/")) {
command = command.substring(1);
}
ServerCommandEvent sce = new ServerCommandEvent((ConsoleCommandSender) sender, command);
serverListener.onServerCommand(sce);
} else if(sender instanceof BlockCommandSender){
MCCommandSender s = new BukkitMCBlockCommandSender((BlockCommandSender)sender);
Static.getAliasCore().alias(command, s);
}
return true;
} else if(cmdName.equalsIgnoreCase("interpreter-on")){
if(sender instanceof ConsoleCommandSender){
int interpreterTimeout = Prefs.InterpreterTimeout();
if(interpreterTimeout != 0){
interpreterUnlockedUntil = (interpreterTimeout * 60 * 1000) + System.currentTimeMillis();
sender.sendMessage("Inpterpreter mode unlocked for " + interpreterTimeout + " minute"
+ (interpreterTimeout==1?"":"s"));
}
} else {
sender.sendMessage("This command can only be run from console.");
}
return true;
} else if (sender instanceof Player && cmdName.equalsIgnoreCase("interpreter")) {
if (!sender.hasPermission("commandhelper.interpreter")) {
sender.sendMessage(MCChatColor.RED + "You do not have permission to run that command");
} else if (!Prefs.EnableInterpreter()) {
sender.sendMessage(MCChatColor.RED + "The interpreter is currently disabled."
+ " Check your preferences file.");
} else if (Prefs.InterpreterTimeout() != 0 && interpreterUnlockedUntil < System.currentTimeMillis()) {
sender.sendMessage(MCChatColor.RED + "Interpreter mode is currently locked. Run \"interpreter-on\""
+ " console to unlock it. If you want to turn this off entirely, set the interpreter-timeout"
+ " option to 0 in " + CommandHelperFileLocations.getDefault().getPreferencesFile().getName());
} else {
interpreterListener.startInterpret(sender.getName());
sender.sendMessage(MCChatColor.YELLOW + "You are now in interpreter mode. Type a dash (-) on a"
+ " line by itself to exit, and >>> to enter multiline mode.");
}
return true;
} else {
MCCommandSender mcsender = BukkitConvertor.BukkitGetCorrectSender(sender);
MCCommand mccmd = new BukkitMCCommand(cmd);
return mccmd.handleCustomCommand(mcsender, commandLabel, args);
}
}
/**
* Joins a string from an array of strings.
*
* @param str
* @param delimiter
* @return
*/
public static String joinString(String[] str, String delimiter) {
if (str.length == 0) {
return "";
}
StringBuilder buffer = new StringBuilder(str[0]);
for (int i = 1; i < str.length; i++) {
buffer.append(delimiter).append(str[i]);
}
return buffer.toString();
}
/**
* Execute a command.
*
* @param player
*
* @param cmd
*/
public static void execCommand(MCPlayer player, String cmd) {
player.chat(cmd);
}
}