diff --git a/MultiServer/pom.xml b/MultiServer/pom.xml new file mode 100644 index 0000000..15881d9 --- /dev/null +++ b/MultiServer/pom.xml @@ -0,0 +1,78 @@ + + + 4.0.0 + ASys Multi server + + + + DmitriyMX + mail@dmiriymx.ru + + + + + UTF-8 + 1.8 + + + asys + multiserver + 0.1 + bundle + + + + asys + api + 0.10 + + + org.osgi + org.osgi.core + 6.0.0 + + + org.apache.felix + org.apache.felix.gogo.runtime + 0.10.0 + + + commons-io + commons-io + 2.5 + + + + + ${project.groupId}.${project.artifactId}-${project.version} + + + org.apache.maven.plugins + maven-compiler-plugin + 3.5.1 + + ${java.version} + ${java.version} + ${project.build.sourceEncoding} + + + + org.apache.felix + maven-bundle-plugin + 3.0.1 + true + + + ${project.name} + ${project.groupId}.${project.artifactId} + asys.multiserver.Activator + asys.api, * + + + + + + \ No newline at end of file diff --git a/MultiServer/src/main/java/asys/multiserver/Activator.java b/MultiServer/src/main/java/asys/multiserver/Activator.java new file mode 100644 index 0000000..7637300 --- /dev/null +++ b/MultiServer/src/main/java/asys/multiserver/Activator.java @@ -0,0 +1,79 @@ +/* + * DmitriyMX + * 2016-08-15 + */ +package asys.multiserver; + +import asys.api.BankObject; +import asys.api.MinecraftServerFactory; +import asys.api.ServerManager; +import org.osgi.framework.BundleActivator; +import org.osgi.framework.BundleContext; +import org.osgi.framework.ServiceRegistration; +import org.osgi.util.tracker.ServiceTracker; + +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Properties; + +import static asys.api.ASysUtils.*; + +public class Activator implements BundleActivator { + private ServiceTracker mcServerFactoryTracker; + private ServiceTracker bankObjectTracker; + private ServiceRegistration commands, serverManager; + private MultiServer multiServer; + + @Override + public void start(BundleContext bundleContext) throws Exception { + mcServerFactoryTracker = new ServiceTracker<>(bundleContext, MinecraftServerFactory.class.getName(), null); + mcServerFactoryTracker.open(); + bankObjectTracker = new ServiceTracker<>(bundleContext, BankObject.class.getName(), null); + bankObjectTracker.open(); + + multiServer = new MultiServer( + loadProps(GetProperty(bundleContext, "asys.config.dir", "conf")), + mcServerFactoryTracker); + multiServer.loadState(bankObjectTracker.getService()); + serverManager = bundleContext.registerService(ServerManager.class.getName(), multiServer, null); + + commands = RegisterCommands(bundleContext, new Commands(multiServer), "asys.server"); + } + + @Override + public void stop(BundleContext bundleContext) throws Exception { + commands.unregister(); + serverManager.unregister(); + multiServer.saveState(bankObjectTracker.getService()); + multiServer = null; + bankObjectTracker.close(); + mcServerFactoryTracker.close(); + } + + private Properties loadProps(String confDir) { + Properties properties = new Properties(); + + final String propsFileName = "asys-multiserver.properties"; + Path propsPath = Paths.get(confDir).resolve(propsFileName); + if (Files.notExists(propsPath)) { + try { + SaveResource(Activator.class.getResourceAsStream("/"+propsFileName), propsPath.toFile()); + } catch (IOException e) { + e.printStackTrace(); + return properties; + } + } + + try { + properties.load(new FileReader(propsPath.toFile())); + return properties; + } catch (IOException e) { + e.printStackTrace(); + } + + return properties; + } +} diff --git a/MultiServer/src/main/java/asys/multiserver/Commands.java b/MultiServer/src/main/java/asys/multiserver/Commands.java new file mode 100644 index 0000000..419acc6 --- /dev/null +++ b/MultiServer/src/main/java/asys/multiserver/Commands.java @@ -0,0 +1,165 @@ +/* + * DmitriyMX + * 2016-08-15 + */ +package asys.multiserver; + +import asys.api.ASysUtils; +import asys.api.Command; +import asys.api.MinecraftServer; +import org.apache.felix.service.command.Descriptor; + +import java.io.IOException; +import java.util.Arrays; +import java.util.StringJoiner; + +public class Commands { + private MultiServer multiServer; + + public Commands(MultiServer multiServer) { + this.multiServer = multiServer; + } + + @Command + @Descriptor("Распечатать текущие настройки модуля") + public void config() { + ASysUtils.Log("------ Config ------"); + ASysUtils.Log("%-20s %s", "BuildScript folder:", multiServer.buildScriptPath.toAbsolutePath()); + ASysUtils.Log("%-20s %s", "Distributive folder:", multiServer.distrPath.toAbsolutePath()); + ASysUtils.Log("%-20s %s", "Servers folder:", multiServer.serversPath.toAbsolutePath()); + } + + @Command + @Descriptor("Развернуть новый сервер") + public void deploy(@Descriptor("тип сервера") String type) { + try { + ASysUtils.Log("New server id: %s", multiServer.deployServer(type)); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Command + @Descriptor("Развернуть новый сервер") + public void deploy(@Descriptor("тип сервера") String type, @Descriptor("количество") int count) { + for (int i = 0; i < count; i++) { + try { + ASysUtils.Log("New server id: %s", multiServer.deployServer(type)); + } catch (IOException e) { + e.printStackTrace(); + break; + } + } + } + + @Command + @Descriptor("Получить список серверов") + public void list() { + ASysUtils.Log("------ List servers ------"); + final String format = " %-9s | %-6s"; + ASysUtils.Log("%10s | %-6s", "ID", "STATUS"); + multiServer.listServers().forEach(mcServer -> ASysUtils.Log( + format, + mcServer.getName(), + mcServer.isAlive() ? "Active" : "Ready")); + } + + @Command + @Descriptor("Получить список серверов") + public void list(@Descriptor("статус") String status) { + ASysUtils.Log("------ List servers ------"); + final String format = " %-9s | %-6s"; + ASysUtils.Log("%10s | %-6s", "ID", "STATUS"); + multiServer.listServers().stream() + .filter(mcServer -> ((status.equalsIgnoreCase("active") && mcServer.isAlive()) || + (status.equalsIgnoreCase("ready") && !mcServer.isAlive()))) + .forEach(mcServer -> ASysUtils.Log( + format, + mcServer.getName(), + mcServer.isAlive() ? "Active" : "Ready")); + } + + @Command + @Descriptor("Получить список типов серверов") + public void types() { + ASysUtils.Log("------ Type servers ------"); + multiServer.getTypes().forEach(type -> ASysUtils.Log(" %s", type)); + } + + @Command + @Descriptor("Старт сервера") + public void start(@Descriptor("id сервера") String serverId) { + MinecraftServer server = multiServer.getServer(serverId); + if (server == null) { + ASysUtils.Log("Server \"%s\" not found", serverId); + } else if (!server.isAlive()) { + server.start(); + ASysUtils.Log("Server \"%s\" started", serverId); + } + } + + @Command + @Descriptor("Остановка сервера") + public void stop(@Descriptor("id сервера") String serverId) { + MinecraftServer server = multiServer.getServer(serverId); + if (server == null) { + ASysUtils.Log("Server \"%s\" not found", serverId); + } else if (server.isAlive()) { + server.stop(); + ASysUtils.Log("Server \"%s\" stoppind", serverId); + } + } + + @Command + @Descriptor("Убить процесс сервера") + public void kill(@Descriptor("id сервера") String serverId) { + MinecraftServer server = multiServer.getServer(serverId); + if (server == null) { + ASysUtils.Log("Server \"%s\" not found", serverId); + } else if (server.isAlive()) { + server.forceStop(); + ASysUtils.Log("Server \"%s\" killing", serverId); + } + } + + @Command + @Descriptor("Отправить на сервер комманду") + public void cmd(@Descriptor("id сервера") String serverId, @Descriptor("коменда") String... command) { + MinecraftServer server = multiServer.getServer(serverId); + if (server == null) { + ASysUtils.Log("Server \"%s\" not found", serverId); + } else if (server.isAlive()) { + StringJoiner sj = new StringJoiner(" "); + Arrays.asList(command).forEach(sj::add); + server.sendCommand(sj.toString()); + } + } + +// @Command + @Descriptor("Получить информацию о сервере") + public void info(@Descriptor("id сервера") String serverId) { + /*TODO вывести информацию о соответствующем сервере + * информация должна содержать следующее: + * - id сервера + * - текущий онлайн + * - максимальный онлайн + * - TPS + * - политику доступа + * - время uptime + * - использумое кол-во оперативной памяти + * - максимально доступное кол-во памяти + */ + } + +// @Command + @Descriptor("Установить серверу политику доступа") + public void accessPolicy(@Descriptor("id сервера") String serverid, String newPolicy) { + //TODO устанавливает соответствующему серверу политику доступа + } + +// @Command + @Descriptor("Удалить сервер (физически)") + public void remove(@Descriptor("id сервера") String serverId) { + //TODO удаляет сервер с диска + } +} diff --git a/MultiServer/src/main/java/asys/multiserver/MultiServer.java b/MultiServer/src/main/java/asys/multiserver/MultiServer.java new file mode 100644 index 0000000..843fba6 --- /dev/null +++ b/MultiServer/src/main/java/asys/multiserver/MultiServer.java @@ -0,0 +1,185 @@ +/* + * DmitriyMX + * 2016-08-15 + */ +package asys.multiserver; + +import asys.api.BankObject; +import asys.api.MinecraftServer; +import asys.api.MinecraftServerFactory; +import asys.api.ServerManager; +import asys.multiserver.buildscript.BuildScript; +import asys.multiserver.buildscript.CommandException; +import org.osgi.util.tracker.ServiceTracker; + +import java.io.File; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; + +public class MultiServer implements ServerManager { + private Map mapMcServers = new HashMap<>(); + private Random random = new Random(System.currentTimeMillis()); + private ServiceTracker mcsfTracker; + Path buildScriptPath, distrPath, serversPath; + + public MultiServer(Properties properties, ServiceTracker mcServerFactoryTracker) { + buildScriptPath = Paths.get(properties.getProperty("buildscript.dir", "scripts")); + distrPath = Paths.get(properties.getProperty("distributive.dir", "distr")); + serversPath = Paths.get(properties.getProperty("servers.dir", "servers")); + this.mcsfTracker = mcServerFactoryTracker; + + File[] serverDirs = serversPath.toFile().listFiles((dir, name) -> dir.isDirectory()); + if (serverDirs != null) { + for(File serverDir :serverDirs) { + Path asysPropsPath = serverDir.toPath().resolve("asys.properties"); + if (Files.exists(asysPropsPath)) { + Properties asysProps = new Properties(); + try { + FileReader fileReader = new FileReader(asysPropsPath.toFile()); + asysProps.load(fileReader); + fileReader.close(); + } catch (IOException e) { +// e.printStackTrace(); + return; + } + + putServer(asysProps, serverDir); + } + } + } + } + + public List getTypes() { + List list = new ArrayList<>(); + String[] files = buildScriptPath.toFile().list((dir, name) -> name.indexOf(' ') == -1 && name.endsWith(".bs")); + if (files != null) { + Arrays.stream(files).map(s -> { + int i = s.lastIndexOf(".bs"); + return s.substring(0, i); + }).forEach(list::add); + } + + return list; + } + + public String deployServer(String type) throws IOException { + String serverId; + Path newServerPath; + final int max = 99; + int _try = 0; + int nId; + do { + if (_try == max*2) throw new IOException("End of free server id"); + nId = random.nextInt(max); + serverId = type + nId; + newServerPath = serversPath.resolve(serverId); + _try++; + } while (Files.exists(newServerPath)); + + Files.createDirectory(newServerPath); + + try { + BuildScript buildScript = BuildScript.loadFromFile(buildScriptPath.resolve(type + ".bs").toFile()); + buildScript.setVariable("servers", serversPath.toAbsolutePath().toString()); + buildScript.setVariable("distrib", distrPath.toAbsolutePath().toString()); + buildScript.setVariable("serverId", serverId); + buildScript.execute(); + } catch (CommandException e) { + throw new IOException(e); + } + + Properties asysProps = new Properties(); + Path serverDirPath = serversPath.resolve(serverId); + Path asysPropPath = serverDirPath.resolve("asys.properties"); + if (Files.exists(asysPropPath)) { + asysProps.load(new FileReader(asysPropPath.toFile())); + } + asysProps.setProperty("server.id", serverId); + if (asysProps.getProperty("server.mainjar", null) == null) { + asysProps.setProperty("server.mainjar", "spigot.jar"); + } + if (asysProps.getProperty("server.port.prefix", null) == null) { + asysProps.setProperty("server.port.prefix", "00"); + } + asysProps.setProperty("server.port", + "2"+asysProps.getProperty("server.port.prefix")+nId); + asysProps.store(new FileWriter(asysPropPath.toFile()), "ASys Server settings"); + + putServer(asysProps, serverDirPath.toFile()); + + return serverId; + } + + private void putServer(Properties asysProps, File serverDir) { + if (asysProps.getProperty("server.id", null) == null) + return; //TODO ошибку бы генерировать + if (asysProps.getProperty("server.port", null) == null) + return; //TODO ошибку бы генерировать + + MinecraftServerFactory serverFactory = null; + try { + serverFactory = mcsfTracker.waitForService(1000L); + } catch (InterruptedException e) { + } + + if (serverFactory == null) + return; //TODO ошибку бы генерировать + + mapMcServers.put( + asysProps.getProperty("server.id"), + serverFactory.createServer( + asysProps.getProperty("server.id"), + serverDir, + asysProps.getProperty("server.mainjar"), + Short.valueOf(asysProps.getProperty("server.port")), //TODO надо сделать защиту от дурака: что если буквы введут? + asysProps.getProperty("server.jvm.args"), + asysProps.getProperty("server.params") + )); + } + + public List listServers() { + return new ArrayList<>(mapMcServers.values()); + } + + @Override + public MinecraftServer getServer(String serverId) { + return mapMcServers.get(serverId); //TODO по хорошему, надо бы возвращать какой-нибудь EmptyPbject, а не null + } + + @Override + public void removeServer(String serverId) { + mapMcServers.remove(serverId); + } + + @SuppressWarnings("unchecked") + void loadState(BankObject bankObject) { + if(bankObject == null) return; + + List serversState = (List) bankObject.get(MultiServer.class.getName()+"#servers"); + if (serversState == null) return; + + serversState.forEach(server -> { + if (server.isAlive()) { + mapMcServers.put(server.getName(), server); + } + }); + } + + void saveState(BankObject bankObject) { + if(bankObject == null) return; + + List serversState = new ArrayList<>(); + mapMcServers.values().forEach(server -> { + if (server.isAlive()) { + serversState.add(server); + } + }); + + bankObject.save(MultiServer.class.getName()+"#servers", serversState); + } +} diff --git a/MultiServer/src/main/java/asys/multiserver/buildscript/BuildScript.java b/MultiServer/src/main/java/asys/multiserver/buildscript/BuildScript.java new file mode 100644 index 0000000..cb68c12 --- /dev/null +++ b/MultiServer/src/main/java/asys/multiserver/buildscript/BuildScript.java @@ -0,0 +1,277 @@ +/* + * DmitriyMX + * 2016-06-23 + */ +package asys.multiserver.buildscript; + +import asys.api.ASysUtils; +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.*; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Скрипт формирования окружения игрового сервера.

+ * + * Команды:
+ * - lnk [источник] [цель] - создание ссылки;
+ * - copy [источник] [цель] - копирование;
+ * - mkdir [путь] - создание папки или древа папок;
+ * - unpack [архив] [папка] - распаковка архива в указанную папку;
+ * - undoparent - отменить запланированные действия вышестоящего скрипта. + */ +public class BuildScript { + List scriptLines = new ArrayList<>(); + private Map variables = new HashMap<>(); + + public static BuildScript loadFromFile(File file) throws IOException, UnknowCommandException { + return new BuildScript(FileUtils.readFileToString(file, Charset.forName("UTF-8"))); + } + + public BuildScript(String script) throws UnknowCommandException { + if (script == null || script.trim().isEmpty()) { + ASysUtils.Log("[WARN] Empty script!"); + return; + } + + if (script.contains("\r\n")) { +// ASysUtils.Log("[WARN] Finded CRLF! Replaced..."); + script = script.replaceAll("\r\n", "\n"); + } + + for (String line : script.split("\n")) { + scriptLines.add(parseCommandLine(line)); + } + } + + public void setVariables(Map variables) { + this.variables = variables; + } + + public void setVariable(String name, String value) { + this.variables.put(name, value); + } + + private String[] parseCommandLine(String line) throws UnknowCommandException { + final StringTokenizer strTok = new StringTokenizer(line, " \"\'", true); + final List preArr = new ArrayList<>(); + byte state = 0; // 0-normal, 1-quoting1, 2-quoting2 + String buff = ""; + boolean foundCmd = false; + + while (strTok.hasMoreTokens()) { + String partLine = strTok.nextToken(); + + if (!foundCmd) { + switch (partLine) { + case "lnk": + case "copy": + case "mkdir": + case "unpack": + case "undoparent": + foundCmd = true; // cmd correct; + partLine = partLine.toLowerCase(); + break; + default: + foundCmd = false; // cmd correct; + } + + if (!foundCmd) { + throw new UnknowCommandException(partLine); + } + } + + if (partLine.equals("\"")) { + if (state == 0) { + state = 1; + continue; + } else if (state == 1){ + state = 0; + preArr.add(buff); + buff = ""; + continue; + } + } + + if (partLine.equals("\'")) { + if (state == 0) { + state = 2; + continue; + } else if (state == 2){ + state = 0; + preArr.add(buff); + buff = ""; + continue; + } + } + + if (state == 1 || state == 2) { + buff += partLine; + continue; + } + + if (partLine.equals(" ")) continue; + + preArr.add(partLine); + } + + return preArr.toArray(new String[preArr.size()]); + } + + public void execute() throws CommandException { + for (String[] cmdline : scriptLines) { + switch (cmdline[0]) { + case "lnk": cmd_lnk(cmdline[1], cmdline[2]); break; + case "copy": cmd_copy(cmdline[1], cmdline[2]); break; + case "mkdir": cmd_mkdir(cmdline[1]); break; + case "unpack": cmd_unpack(cmdline[1], cmdline[2]); break; + case "undoparent": cmd_undo(); break; + default: return; + } + } + } + + private String applyVariables(String string) { + if (variables != null && !variables.isEmpty()) { + for (Map.Entry entry : variables.entrySet()) { + string = string.replace("%"+entry.getKey()+"%", entry.getValue()); + } + } + + return string; + } + + /* Создание символьной ссылки */ + private void cmd_lnk(String source, String target) throws CommandException { + try { + Files.createSymbolicLink( + Paths.get(applyVariables(target)), + Paths.get(applyVariables(source))); + } catch (IOException e) { + throw new CommandException("LNK", e); + } + } + + /* Коирование */ + private void cmd_copy(String source, String target) throws CommandException { + //TODO надо изюавиться от излишних Path.toFile() + Path sourcePath = Paths.get(applyVariables(source)); + if (!sourcePath.toFile().exists()) { + throw new CommandException(String.format("COPY: source not found %s [%s]", source, sourcePath.toAbsolutePath())); + } + + Path targetPath = Paths.get(applyVariables(target)); + if (!targetPath.toFile().exists()) { + if (sourcePath.toFile().isDirectory()) { + try { + FileUtils.copyDirectory(sourcePath.toFile(), targetPath.toFile()); + } catch (IOException e) { + throw new CommandException("COPY: error copy directory", e); + } + } else if (sourcePath.toFile().isFile()) { + try { + FileUtils.copyFile(sourcePath.toFile(), targetPath.toFile()); + } catch (IOException e) { + throw new CommandException("COPY: error copy file", e); + } + } else { + throw new CommandException("COPY: unknow type source file"); + } + } else { + if (sourcePath.toFile().isDirectory()) { + if (targetPath.toFile().isFile()) { + throw new CommandException("COPY: can't be copy dir to file"); + } else { + try { + FileUtils.copyDirectory(sourcePath.toFile(), targetPath.toFile()); + } catch (IOException e) { + throw new CommandException("COPY: error copy directory", e); + } + } + } else if (sourcePath.toFile().isFile()) { + if (targetPath.toFile().isDirectory()) { + try { + FileUtils.copyFileToDirectory(sourcePath.toFile(), targetPath.toFile()); + } catch (IOException e) { + throw new CommandException("COPY: error copy file", e); + } + } else if (targetPath.toFile().isFile()) { + try { + FileUtils.copyFile(sourcePath.toFile(), targetPath.toFile()); + } catch (IOException e) { + throw new CommandException("COPY: error copy file", e); + } + } + } + } + } + + /* Создание папки/древа папок + * параметры: цель */ + private void cmd_mkdir(String dir) throws CommandException { + File treeDir = Paths.get(applyVariables(dir)).toFile(); + if (treeDir.mkdirs() && !treeDir.exists()) { + throw new CommandException(String.format("MKDIR: can't create dirs %s [%s]", dir, treeDir.getAbsolutePath())); + } + } + + private void cmd_unpack(String source, String target) throws CommandException { + Path sourcePath = Paths.get(applyVariables(source)); + if (!sourcePath.toFile().exists()) { + throw new CommandException(String.format("UNPACK: source not found %s [%s]", source, sourcePath.toFile().getAbsolutePath())); + } + + if (!sourcePath.toFile().isFile()) { + throw new CommandException(String.format("UNPACK: source is not file %s [%s]", source, sourcePath.toFile().getAbsolutePath())); + } + + Path targetPath = Paths.get(applyVariables(target)); + if (!targetPath.toFile().exists()) { + if (targetPath.toFile().mkdirs() && !targetPath.toFile().exists()) { + throw new CommandException(String.format("UNPACK: can't create dir %s [%s]", target, targetPath.toFile().getAbsolutePath())); + } + } else if (targetPath.toFile().exists() && targetPath.toFile().isFile()) { + throw new CommandException(String.format("UNPACK: target can't be file %s [%s]", target, targetPath.toFile().getAbsolutePath())); + } + + // unzip + byte[] buffer = new byte[65536]; + try { + ZipInputStream zis = new ZipInputStream(new FileInputStream(sourcePath.toFile())); + + ZipEntry ze; + while ((ze = zis.getNextEntry()) != null) { + if (ze.isDirectory()) continue; + String fileName = ze.getName(); + File newFile = new File(targetPath.toFile(), fileName); + Files.createDirectories(Paths.get(newFile.getParent())); + + FileOutputStream fos = new FileOutputStream(newFile); + int len; + while ((len = zis.read(buffer)) > 0) { + fos.write(buffer, 0, len); + } + fos.close(); + } + + zis.closeEntry(); + zis.close(); + } catch (IOException e) { + throw new CommandException("UNPACK", e); + } + } + + private void cmd_undo() { + // empty + } +} + diff --git a/MultiServer/src/main/java/asys/multiserver/buildscript/CommandException.java b/MultiServer/src/main/java/asys/multiserver/buildscript/CommandException.java new file mode 100644 index 0000000..d7b3e57 --- /dev/null +++ b/MultiServer/src/main/java/asys/multiserver/buildscript/CommandException.java @@ -0,0 +1,15 @@ +/* + * DmitriyMX + * 2016-06-28 + */ +package asys.multiserver.buildscript; + +public class CommandException extends Exception { + CommandException(String message) { + super(message); + } + + CommandException(String message, Throwable cause) { + super(message, cause); + } +} \ No newline at end of file diff --git a/MultiServer/src/main/java/asys/multiserver/buildscript/UnknowCommandException.java b/MultiServer/src/main/java/asys/multiserver/buildscript/UnknowCommandException.java new file mode 100644 index 0000000..709806e --- /dev/null +++ b/MultiServer/src/main/java/asys/multiserver/buildscript/UnknowCommandException.java @@ -0,0 +1,11 @@ +/* + * DmitriyMX + * 2016-06-23 + */ +package asys.multiserver.buildscript; + +public class UnknowCommandException extends CommandException { + UnknowCommandException(String message) { + super(message); + } +} diff --git a/MultiServer/src/main/resources/asys-multiserver.properties b/MultiServer/src/main/resources/asys-multiserver.properties new file mode 100644 index 0000000..88114c0 --- /dev/null +++ b/MultiServer/src/main/resources/asys-multiserver.properties @@ -0,0 +1,3 @@ +buildscript.dir=scripts +distributive.dir=distr +servers.dir=servers \ No newline at end of file