Archived
0

Merge remote-tracking branch 'origin/dev-webconsole' into local-merge

# Conflicts:
#	bridge-protocol/build.gradle
#	bridge/src/main/java/asys/bridge/bukkit/BridgePlugin.java
#	bridge/src/main/java/asys/bridge/client/ClientPacketHandler.java
#	mcserver-manager/build.gradle
#	mcserver-manager/src/main/java/asys/mcsmanager/server/ServerPacketHandler.java
This commit is contained in:
2017-06-08 13:27:05 +03:00
19 changed files with 905 additions and 55 deletions

View File

@@ -1,5 +1,5 @@
group = 'asys' group = 'asys'
version = '0.3-SNAPSHOT' version = '0.4-SNAPSHOT'
task jar(type: Jar, overwrite: true) { task jar(type: Jar, overwrite: true) {
// не собирать jar // не собирать jar

View File

@@ -0,0 +1,32 @@
/*
* DmitriyMX <d.mihailov@samson-rus.com>
* 2017-06-05
*/
package asys.mcsmanager.packets;
import io.netty.buffer.ByteBuf;
public class SC_Command extends Packet {
private String command;
public SC_Command() {
}
public SC_Command(String command) {
this.command = command;
}
public String getCommand() {
return command;
}
@Override
public void readSelfData(ByteBuf buffer) {
command = readString(buffer);
}
@Override
public void writeSelfData(ByteBuf buffer) {
writeString(buffer, command);
}
}

View File

@@ -19,12 +19,15 @@ public class PacketDecoder extends ReplayingDecoder<Packet> {
protected void decode(ChannelHandlerContext contect, ByteBuf inBuf, List<Object> out) throws Exception { protected void decode(ChannelHandlerContext contect, ByteBuf inBuf, List<Object> out) throws Exception {
int id = inBuf.readUnsignedByte(); int id = inBuf.readUnsignedByte();
Class<? extends Packet> pktClass = contect.channel().attr(KNOWN_PACKETS).get().get(id); Class<? extends Packet> pktClass = contect.channel().attr(KNOWN_PACKETS).get().get(id);
if (pktClass == null) return; if (pktClass == null) return; //TODO надо бы в логгере писать про отсутствующий пакет
if (contect.channel().attr(KNOWN_HANDLERS).get().containsKey(pktClass)) { if (contect.channel().attr(KNOWN_HANDLERS).get().containsKey(pktClass)) {
Packet packet = pktClass.newInstance(); Packet packet = pktClass.newInstance();
packet.readSelfData(inBuf); packet.readSelfData(inBuf);
out.add(packet); out.add(packet);
} else {
//TODO по хорошему, надо информровать, что отсутствует обработчик пакета
inBuf.skipBytes(inBuf.readableBytes());
} }
} }
} }

View File

@@ -13,25 +13,19 @@ import org.bukkit.command.Command;
import org.bukkit.command.CommandSender; import org.bukkit.command.CommandSender;
import org.bukkit.plugin.java.JavaPlugin; import org.bukkit.plugin.java.JavaPlugin;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class BridgePlugin extends JavaPlugin { public class BridgePlugin extends JavaPlugin {
public static BridgePlugin INSTANCE; public static BridgePlugin INSTANCE;
private String appConnect = "ASys"; private String appConnect = "ASys";
private Client client; private Client client;
private ScheduledExecutorService ses; private TaskTicker connectTicker, pingTicker;
private ScheduledFuture<?> sesFuture, sesPingFuture;
private int tryConnect = 0; private int tryConnect = 0;
private BridgeLoggerAppender loggerAppender; private BridgeLoggerAppender loggerAppender;
private boolean needReconnect = true;
@Override @Override
public void onLoad() { public void onLoad() {
saveDefaultConfig();
if (getConfig().getInt("mode") == 1) { if (getConfig().getInt("mode") == 1) {
((Logger) LogManager.getRootLogger()).addAppender(loggerAppender = new BridgeLoggerAppender()); ((Logger)LogManager.getRootLogger()).addAppender(loggerAppender = new BridgeLoggerAppender());
} else { } else {
this.appConnect = "Zond"; this.appConnect = "Zond";
} }
@@ -42,16 +36,16 @@ public class BridgePlugin extends JavaPlugin {
public void onEnable() { public void onEnable() {
if (INSTANCE == null) { if (INSTANCE == null) {
INSTANCE = this; INSTANCE = this;
saveDefaultConfig();
startReconnect(); startReconnect();
} }
} }
@Override @Override
public void onDisable() { public void onDisable() {
if (sesFuture != null) { setNeedReconnect(false);
sesFuture.cancel(false); stopReconnect();
}
stopPing(); stopPing();
if (client.isConnected()) { if (client.isConnected()) {
@@ -70,8 +64,8 @@ public class BridgePlugin extends JavaPlugin {
public void startReconnect() { public void startReconnect() {
client = new Client(); client = new Client();
ses = Executors.newScheduledThreadPool(2); connectTicker = new TaskTicker().setStepTimeMs(5000L);
sesFuture = ses.scheduleAtFixedRate(() -> { connectTicker.setTask(() -> {
getLogger().info(String.format("Connect(%d) to %s...", ++tryConnect, appConnect)); getLogger().info(String.format("Connect(%d) to %s...", ++tryConnect, appConnect));
if (getConfig().getInt("mode") == 1) { if (getConfig().getInt("mode") == 1) {
client.connect(getConfig().getString("host"), getConfig().getInt("port")); client.connect(getConfig().getString("host"), getConfig().getInt("port"));
@@ -84,13 +78,12 @@ public class BridgePlugin extends JavaPlugin {
getLogger().warning( getLogger().warning(
String.format("Connection(%d) fail. Try reconnect...", tryConnect)); String.format("Connection(%d) fail. Try reconnect...", tryConnect));
} }
}, 0L, 5L, TimeUnit.SECONDS); }).start();
} }
public void stopReconnect() { public void stopReconnect() {
if (sesFuture != null) { if (connectTicker != null) {
sesFuture.cancel(false); connectTicker.stop();
sesFuture = null;
tryConnect = 0; tryConnect = 0;
} }
} }
@@ -99,7 +92,8 @@ public class BridgePlugin extends JavaPlugin {
if (getConfig().getInt("mode") == 1) { if (getConfig().getInt("mode") == 1) {
getLoggerAppender().setChannel(channel); getLoggerAppender().setChannel(channel);
} }
sesPingFuture = ses.scheduleAtFixedRate(() -> { pingTicker = new TaskTicker().setStepTimeMs(5000L);
pingTicker.setTask(() -> {
channel.write(new CS_Ping( channel.write(new CS_Ping(
System.currentTimeMillis(), System.currentTimeMillis(),
20.0D, //FIXME 20.0D, //FIXME
@@ -115,19 +109,27 @@ public class BridgePlugin extends JavaPlugin {
getLogger().warning("Try reconnect..."); getLogger().warning("Try reconnect...");
startReconnect(); startReconnect();
} }
}, 0L, 5L, TimeUnit.SECONDS); }).start();
} }
public void stopPing() { public void stopPing() {
if (getConfig().getInt("mode") == 1) { if (getConfig().getInt("mode") == 1) {
getLoggerAppender().setChannel(null); getLoggerAppender().setChannel(null);
} }
if (sesPingFuture != null) { if (pingTicker != null) {
sesPingFuture.cancel(false); pingTicker.stop();
} }
} }
private BridgeLoggerAppender getLoggerAppender() { public BridgeLoggerAppender getLoggerAppender() {
return loggerAppender; return loggerAppender;
} }
public boolean isNeedReconnect() {
return needReconnect;
}
public void setNeedReconnect(boolean needReconnect) {
this.needReconnect = needReconnect;
}
} }

View File

@@ -0,0 +1,48 @@
/*
* DmitriyMX <d.mihailov@samson-rus.com>
* 2017-05-18
*/
package asys.bridge.bukkit;
public class TaskTicker implements Runnable {
private Runnable task;
private long stepTimeMs = 1000L;
private Thread thread;
private boolean loop = false;
TaskTicker setTask(Runnable task) {
this.task = task;
return this;
}
TaskTicker setStepTimeMs(long stepTimeMs) {
this.stepTimeMs = stepTimeMs;
return this;
}
void start() {
thread = new Thread(this, "TaskTicker");
loop = true;
thread.start();
}
void stop() {
loop = false;
if (thread != null) {
thread.interrupt();
}
}
@Override
public void run() {
while (loop || !Thread.currentThread().isInterrupted()) {
task.run();
try {
Thread.sleep(stepTimeMs);
} catch (InterruptedException e) {
break;
}
}
}
}

View File

@@ -26,13 +26,15 @@ public class ClientPacketHandler extends ChannelInboundHandlerAdapter implements
private static final BiMap<Integer, Class<? extends Packet>> pingPackets = ImmutableBiMap.of( private static final BiMap<Integer, Class<? extends Packet>> pingPackets = ImmutableBiMap.of(
3, CS_Ping.class, 3, CS_Ping.class,
4, CS_ConsoleMessage.class 4, CS_ConsoleMessage.class,
5, SC_Command.class
); );
ClientPacketHandler() { ClientPacketHandler() {
if (handshakeHandlers == null) { if (handshakeHandlers == null) {
handshakeHandlers = ImmutableMap.of( handshakeHandlers = ImmutableMap.of(
SC_HandshakeResult.class, this SC_HandshakeResult.class, this,
SC_Command.class, this
); );
} }
} }
@@ -61,11 +63,12 @@ public class ClientPacketHandler extends ChannelInboundHandlerAdapter implements
@Override @Override
public void channelInactive(ChannelHandlerContext context) throws Exception { public void channelInactive(ChannelHandlerContext context) throws Exception {
if (BridgePlugin.INSTANCE != null) { if (BridgePlugin.INSTANCE != null) {
BridgePlugin.INSTANCE.getLogger().warning("Lost connection!");
BridgePlugin.INSTANCE.stopPing(); BridgePlugin.INSTANCE.stopPing();
BridgePlugin.INSTANCE.getLogger().warning("Try reconnect..."); if (BridgePlugin.INSTANCE.isNeedReconnect()) {
BridgePlugin.INSTANCE.getLogger().warning("Lost connection! Try reconnect...");
BridgePlugin.INSTANCE.startReconnect(); BridgePlugin.INSTANCE.startReconnect();
} }
}
context.channel().attr(KNOWN_PACKETS).remove(); context.channel().attr(KNOWN_PACKETS).remove();
context.channel().attr(KNOWN_HANDLERS).remove(); context.channel().attr(KNOWN_HANDLERS).remove();
super.channelInactive(context); super.channelInactive(context);
@@ -74,16 +77,32 @@ public class ClientPacketHandler extends ChannelInboundHandlerAdapter implements
@Override @Override
public void handle(Packet packet, ChannelHandlerContext context) { public void handle(Packet packet, ChannelHandlerContext context) {
BridgePlugin.INSTANCE.getLogger().info("handle : " + packet.getClass().getSimpleName()); BridgePlugin.INSTANCE.getLogger().info("handle : " + packet.getClass().getSimpleName());
if (packet instanceof SC_HandshakeResult) {
if (BridgePlugin.INSTANCE.getConfig().getInt("mode") == 1) { if (BridgePlugin.INSTANCE.getConfig().getInt("mode") == 1) {
SC_HandshakeResult pkt = (SC_HandshakeResult) packet; handleHandshakeResult((SC_HandshakeResult) packet, context);
if (pkt.getErrorCode() != 0) { }
} else if (packet instanceof SC_Command) {
handleCommand((SC_Command) packet);
}
}
private void handleHandshakeResult(SC_HandshakeResult packet, ChannelHandlerContext context) {
if (packet.getErrorCode() != 0) {
BridgePlugin.INSTANCE.getLogger().severe( BridgePlugin.INSTANCE.getLogger().severe(
String.format("Handshake: #%d %s", pkt.getErrorCode(), pkt.getMessage())); String.format("Handshake: #%d %s", packet.getErrorCode(), packet.getMessage()));
BridgePlugin.INSTANCE.setNeedReconnect(false);
} else { } else {
context.channel().attr(KNOWN_PACKETS).set(pingPackets); context.channel().attr(KNOWN_PACKETS).set(pingPackets);
BridgePlugin.INSTANCE.getLogger().info("Handshake: OK"); BridgePlugin.INSTANCE.getLogger().info("Handshake: OK");
BridgePlugin.INSTANCE.startPing(context.channel()); BridgePlugin.INSTANCE.startPing(context.channel());
} }
} }
private void handleCommand(SC_Command packet) {
BridgePlugin.INSTANCE.getLogger().info("Command: " + packet.getCommand());
BridgePlugin.INSTANCE.getServer().dispatchCommand(
BridgePlugin.INSTANCE.getServer().getConsoleSender(),
packet.getCommand()
);
} }
} }

View File

@@ -1,5 +1,5 @@
group = 'asys' group = 'asys'
version = '0.8.7-SNAPSHOT' version = '0.8.13-SNAPSHOT'
apply plugin: 'osgi' apply plugin: 'osgi'
@@ -16,6 +16,7 @@ jar {
'!asys.mcsmanager.packets.*', '!asys.mcsmanager.packets.*',
'io.netty.buffer;version="[4.0,5)"', 'io.netty.buffer;version="[4.0,5)"',
'io.netty.handler.codec;version="[4.0,5)"', 'io.netty.handler.codec;version="[4.0,5)"',
'io.netty.handler.codec.http;version="[4.0,5)"',
'*' '*'
} }
@@ -32,4 +33,5 @@ dependencies {
compile project(':webinterface') compile project(':webinterface')
include files(project(':bridge-protocol').sourceSets.main.output.classesDir) include files(project(':bridge-protocol').sourceSets.main.output.classesDir)
compile group: 'io.netty', name: 'netty-codec', version: nettyVersion compile group: 'io.netty', name: 'netty-codec', version: nettyVersion
compile group: 'io.netty', name: 'netty-codec-http', version: nettyVersion
} }

View File

@@ -5,7 +5,6 @@
package asys.mcsmanager; package asys.mcsmanager;
import asys.api.Config; import asys.api.Config;
import asys.mcsmanager.server.Server;
import asys.webinterface.api.Webinterface; import asys.webinterface.api.Webinterface;
import org.osgi.framework.BundleActivator; import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext; import org.osgi.framework.BundleContext;
@@ -20,7 +19,8 @@ public class Activator implements BundleActivator, ServiceListener {
private ServiceTracker<?, Webinterface> serviceTracker; private ServiceTracker<?, Webinterface> serviceTracker;
private MCSM_WebModule module; private MCSM_WebModule module;
private Webinterface webinterface; private Webinterface webinterface;
private Server serverManager; private asys.mcsmanager.server.Server serverManager;
private asys.mcsmanager.websocket.Server webconsoleServer;
@Override @Override
public void start(BundleContext context) throws Exception { public void start(BundleContext context) throws Exception {
@@ -43,13 +43,21 @@ public class Activator implements BundleActivator, ServiceListener {
int port = config.getInt("asys.mcsmanager.port", 8779); int port = config.getInt("asys.mcsmanager.port", 8779);
String passcode = config.getString("asys.mcsmanager.passcode", "testpasscode"); String passcode = config.getString("asys.mcsmanager.passcode", "testpasscode");
logger.debug("Start server manager: {}:{}", host, port); logger.debug("Start server manager: {}:{}", host, port);
serverManager = new Server(); serverManager = new asys.mcsmanager.server.Server();
serverManager.start(host, port, passcode, manager); serverManager.start(host, port, passcode, manager);
host = config.getString("asys.mcsmanager.webconsole.host", "127.0.0.1");
port = config.getInt("asys.mcsmanager.webconsole.port", 8770);
logger.debug("Start webconsole server: {}:{}", host, port);
webconsoleServer = new asys.mcsmanager.websocket.Server();
webconsoleServer.start(host, port, manager);
serviceConfigTracker.close(); serviceConfigTracker.close();
} }
@Override @Override
public void stop(BundleContext context) throws Exception { public void stop(BundleContext context) throws Exception {
webconsoleServer.shutdown();
serverManager.shutdown(); serverManager.shutdown();
if (webinterface != null) { if (webinterface != null) {

View File

@@ -26,6 +26,7 @@ public class MCSM_WebModule extends WebModule {
private final String MODULE_NAME = "mcsmanager"; private final String MODULE_NAME = "mcsmanager";
private final String MODULE_URL = "/"+MODULE_NAME; private final String MODULE_URL = "/"+MODULE_NAME;
private final Pattern URL_PATTERN_JS = Pattern.compile(MODULE_URL+"/(\\w+)\\.js"); private final Pattern URL_PATTERN_JS = Pattern.compile(MODULE_URL+"/(\\w+)\\.js");
private final Pattern URL_PATTERN_CSS = Pattern.compile(MODULE_URL+"/(\\w+)\\.css");
private Manager manager; private Manager manager;
MCSM_WebModule(Manager manager) { MCSM_WebModule(Manager manager) {
@@ -66,6 +67,19 @@ public class MCSM_WebModule extends WebModule {
this.sendContent(httpExchange, 0, stream); this.sendContent(httpExchange, 0, stream);
return true; return true;
} }
//FIXME дублирование кода
matcher = URL_PATTERN_CSS.matcher(urlPath);
if (matcher.find()) {
InputStream stream = getClass().getResourceAsStream("/" + matcher.group(1) + ".css");
if (stream == null) {
this.sendHttpCode(httpExchange, 404, "not found");
return true;
}
httpExchange.getResponseHeaders().add("Content-Type", "text/css;charset=utf-8");
this.sendContent(httpExchange, 0, stream);
return true;
}
} }
return false; return false;

View File

@@ -4,7 +4,11 @@
*/ */
package asys.mcsmanager; package asys.mcsmanager;
import asys.mcsmanager.packets.CS_ConsoleMessage;
import asys.mcsmanager.packets.CS_Ping; import asys.mcsmanager.packets.CS_Ping;
import asys.mcsmanager.packets.SC_Command;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -13,19 +17,43 @@ import java.util.*;
public class Manager { public class Manager {
private Logger logger = LoggerFactory.getLogger(Manager.class); private Logger logger = LoggerFactory.getLogger(Manager.class);
private Map<String, ServerInfo> serversMap = new HashMap<>(); private Map<String, ServerInfo> serversMap = new HashMap<>();
private Map<String, Channel> serverChannels = new HashMap<>();
private List<Channel> webconsoleListener = new ArrayList<>();
/** /**
* Добавляем в список ClientID * Добавляем в список ClientID
* @param clientId id сервера. Чувствителен к регистру * @param clientId id сервера. Чувствителен к регистру
* @return <code>false</code>, если сервер с таким id уже имеется * @return <code>false</code>, если сервер с таким id уже имеется
*/ */
public boolean addClientId(String clientId) { public boolean addClientId(String clientId, Channel channel) {
if (serversMap.containsKey(clientId)) { if (serversMap.containsKey(clientId)) {
return false; return false;
} }
logger.debug("addClientId: {}", clientId); logger.debug("addClientId: {}", clientId);
serversMap.put(clientId, new ServerInfo(clientId)); serversMap.put(clientId, new ServerInfo(clientId));
serverChannels.put(clientId, channel);
return true;
}
public void removeClientId(String clientId) {
if (clientId != null) {
serverChannels.remove(clientId);
}
}
public boolean sendCommand(String clientId, String command) {
/*
if (serverChannels.containsKey(clientId)) {
serverChannels.get(clientId).writeAndFlush(new SC_Command(command));
return true;
} else {
return false;
}
*/
//FIXME временный костыль
Channel channel = serverChannels.entrySet().iterator().next().getValue();
channel.writeAndFlush(new SC_Command(command));
return true; return true;
} }
@@ -47,4 +75,22 @@ public class Manager {
public ServerInfo getInfo(String clientId) { public ServerInfo getInfo(String clientId) {
return serversMap.get(clientId); return serversMap.get(clientId);
} }
public void addWebConsoleListener(Channel channel) {
this.webconsoleListener.add(channel);
}
public void removeWebConsoleListener(Channel channel) {
this.webconsoleListener.remove(channel);
}
public void sendBroadcastToWebConsoleListeners(CS_ConsoleMessage message) {
for (Channel channel : this.webconsoleListener) {
channel.writeAndFlush(new TextWebSocketFrame(String.format(
"[L:%d] %s",
message.getLevel(),
message.getMessage()
)));
}
}
} }

View File

@@ -28,7 +28,8 @@ class ServerPacketHandler extends ChannelInboundHandlerAdapter implements IPacke
private static final BiMap<Integer, Class<? extends Packet>> pingPackets = ImmutableBiMap.of( private static final BiMap<Integer, Class<? extends Packet>> pingPackets = ImmutableBiMap.of(
3, CS_Ping.class, 3, CS_Ping.class,
4, CS_ConsoleMessage.class 4, CS_ConsoleMessage.class,
5, SC_Command.class
); );
private static Map<Class<? extends Packet>, IPacketHandler> pingHandlers; private static Map<Class<? extends Packet>, IPacketHandler> pingHandlers;
@@ -56,6 +57,12 @@ class ServerPacketHandler extends ChannelInboundHandlerAdapter implements IPacke
super.channelActive(context); super.channelActive(context);
} }
@Override
public void channelInactive(ChannelHandlerContext context) throws Exception {
manager.removeClientId(context.channel().attr(CLIENTID).get());
super.channelInactive(context);
}
@Override @Override
public void handle(Packet packet, ChannelHandlerContext context) { public void handle(Packet packet, ChannelHandlerContext context) {
if (packet.getClass() == CS_Handshake.class) { if (packet.getClass() == CS_Handshake.class) {
@@ -77,7 +84,7 @@ class ServerPacketHandler extends ChannelInboundHandlerAdapter implements IPacke
return; return;
} }
if (!manager.addClientId(packet.getClientId())) { if (!manager.addClientId(packet.getClientId(), context.channel())) {
try { try {
context.channel().writeAndFlush(HandshakeResult.CLIENTID_EXISTS).sync().channel().close(); context.channel().writeAndFlush(HandshakeResult.CLIENTID_EXISTS).sync().channel().close();
} catch (InterruptedException ignore) { } catch (InterruptedException ignore) {
@@ -98,6 +105,6 @@ class ServerPacketHandler extends ChannelInboundHandlerAdapter implements IPacke
} }
private void handleCSConsoleMessage(CS_ConsoleMessage packet) { private void handleCSConsoleMessage(CS_ConsoleMessage packet) {
LoggerFactory.getLogger(getClass()).debug("[L:{}] {}", packet.getLevel(), packet.getMessage()); manager.sendBroadcastToWebConsoleListeners(packet);
} }
} }

View File

@@ -0,0 +1,42 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-05-09
*/
package asys.mcsmanager.websocket;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketFrame;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static asys.mcsmanager.websocket.Server.manager;
public class FrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
private final Logger logger = LoggerFactory.getLogger(FrameHandler.class);
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
manager.addWebConsoleListener(ctx.channel());
super.channelActive(ctx);
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
manager.removeWebConsoleListener(ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
if (frame instanceof TextWebSocketFrame) {
String requestText = ((TextWebSocketFrame)frame).text();
if (requestText.startsWith(":")) {
//FIXME убрать костыли
manager.sendCommand(null, requestText.substring(1));
}
} else {
logger.warn("unsupport frame type: {}", frame.getClass().getName());
}
}
}

View File

@@ -0,0 +1,59 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-05-08
*/
package asys.mcsmanager.websocket;
import asys.mcsmanager.Manager;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
public class Server {
static Manager manager;
private EventLoopGroup bossGroup, workerGroup;
public void start(String host, int port, Manager manager) {
Server.manager = manager;
bossGroup = new NioEventLoopGroup(1);
workerGroup = new NioEventLoopGroup();
ServerBootstrap serverBootstrap = createServerBootstrap();
serverBootstrap.bind(host, port);
}
public void shutdown() {
workerGroup.shutdownGracefully();
bossGroup.shutdownGracefully();
}
private ServerBootstrap createServerBootstrap() {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(createChannelInitializer());
return bootstrap;
}
private ChannelInitializer createChannelInitializer() {
return new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(
new HttpServerCodec(),
new HttpObjectAggregator(65536),
new WebSocketServerProtocolHandler("/", null, true),
new FrameHandler()
);
}
};
}
}

View File

@@ -0,0 +1,333 @@
/* ansi_up.js
* author : Dru Nelson
* license : MIT
* http://github.com/drudru/ansi_up
*/
(function (factory) {
var v;
if (typeof module === "object" && typeof module.exports === "object") {
v = factory(require, exports);
if ("undefined" !== typeof v) module.exports = v;
}
else if ("function" === typeof define && define.amd) {
define(["require", "exports"], factory);
}
else {
var req, exp = {};
v = factory(req, exp);
window.AnsiUp = exp.default;
}
})(function (require, exports) {
"use strict";
function rgx(tmplObj) {
var subst = [];
for (var _i = 1; _i < arguments.length; _i++) {
subst[_i - 1] = arguments[_i];
}
var regexText = tmplObj.raw[0];
var wsrgx = /^\s+|\s+\n|\s+#[\s\S]+?\n/gm;
var txt2 = regexText.replace(wsrgx, '');
return new RegExp(txt2, 'm');
}
var AnsiUp = (function () {
function AnsiUp() {
this.VERSION = "2.0.0";
this.ansi_colors = [
[
{ rgb: [0, 0, 0], class_name: "ansi-black" },
{ rgb: [187, 0, 0], class_name: "ansi-red" },
{ rgb: [0, 187, 0], class_name: "ansi-green" },
{ rgb: [187, 187, 0], class_name: "ansi-yellow" },
{ rgb: [0, 0, 187], class_name: "ansi-blue" },
{ rgb: [187, 0, 187], class_name: "ansi-magenta" },
{ rgb: [0, 187, 187], class_name: "ansi-cyan" },
{ rgb: [255, 255, 255], class_name: "ansi-white" }
],
[
{ rgb: [85, 85, 85], class_name: "ansi-bright-black" },
{ rgb: [255, 85, 85], class_name: "ansi-bright-red" },
{ rgb: [0, 255, 0], class_name: "ansi-bright-green" },
{ rgb: [255, 255, 85], class_name: "ansi-bright-yellow" },
{ rgb: [85, 85, 255], class_name: "ansi-bright-blue" },
{ rgb: [255, 85, 255], class_name: "ansi-bright-magenta" },
{ rgb: [85, 255, 255], class_name: "ansi-bright-cyan" },
{ rgb: [255, 255, 255], class_name: "ansi-bright-white" }
]
];
this.htmlFormatter = {
transform: function (fragment, instance) {
var txt = fragment.text;
if (txt.length === 0)
return txt;
if (instance._escape_for_html)
txt = instance.old_escape_for_html(txt);
if (!fragment.bright && fragment.fg === null && fragment.bg === null)
return txt;
var styles = [];
var classes = [];
var fg = fragment.fg;
var bg = fragment.bg;
if (fg === null && fragment.bright)
fg = instance.ansi_colors[1][7];
if (!instance._use_classes) {
if (fg)
styles.push("color:rgb(" + fg.rgb.join(',') + ")");
if (bg)
styles.push("background-color:rgb(" + bg.rgb + ")");
}
else {
if (fg) {
if (fg.class_name !== 'truecolor') {
classes.push(fg.class_name + "-fg");
}
else {
styles.push("color:rgb(" + fg.rgb.join(',') + ")");
}
}
if (bg) {
if (bg.class_name !== 'truecolor') {
classes.push(bg.class_name + "-bg");
}
else {
styles.push("background-color:rgb(" + bg.rgb.join(',') + ")");
}
}
}
var class_string = '';
var style_string = '';
if (classes.length)
class_string = " class=\"" + classes.join(' ') + "\"";
if (styles.length)
style_string = " style=\"" + styles.join(';') + "\"";
return "<span" + class_string + style_string + ">" + txt + "</span>";
},
compose: function (segments, instance) {
return segments.join("");
}
};
this.textFormatter = {
transform: function (fragment, instance) {
return fragment.text;
},
compose: function (segments, instance) {
return segments.join("");
}
};
this.setup_256_palette();
this._use_classes = false;
this._escape_for_html = true;
this.bright = false;
this.fg = this.bg = null;
this._buffer = '';
}
Object.defineProperty(AnsiUp.prototype, "use_classes", {
get: function () {
return this._use_classes;
},
set: function (arg) {
this._use_classes = arg;
},
enumerable: true,
configurable: true
});
Object.defineProperty(AnsiUp.prototype, "escape_for_html", {
get: function () {
return this._escape_for_html;
},
set: function (arg) {
this._escape_for_html = arg;
},
enumerable: true,
configurable: true
});
AnsiUp.prototype.setup_256_palette = function () {
var _this = this;
this.palette_256 = [];
this.ansi_colors.forEach(function (palette) {
palette.forEach(function (rec) {
_this.palette_256.push(rec);
});
});
var levels = [0, 95, 135, 175, 215, 255];
for (var r = 0; r < 6; ++r) {
for (var g = 0; g < 6; ++g) {
for (var b = 0; b < 6; ++b) {
var col = { rgb: [levels[r], levels[g], levels[b]], class_name: 'truecolor' };
this.palette_256.push(col);
}
}
}
var grey_level = 8;
for (var i = 0; i < 24; ++i, grey_level += 10) {
var gry = { rgb: [grey_level, grey_level, grey_level], class_name: 'truecolor' };
this.palette_256.push(gry);
}
};
AnsiUp.prototype.old_escape_for_html = function (txt) {
return txt.replace(/[&<>]/gm, function (str) {
if (str === "&")
return "&amp;";
if (str === "<")
return "&lt;";
if (str === ">")
return "&gt;";
});
};
AnsiUp.prototype.old_linkify = function (txt) {
return txt.replace(/(https?:\/\/[^\s]+)/gm, function (str) {
return "<a href=\"" + str + "\">" + str + "</a>";
});
};
AnsiUp.prototype.detect_incomplete_ansi = function (txt) {
return !(/.*?[\x40-\x7e]/.test(txt));
};
AnsiUp.prototype.detect_incomplete_link = function (txt) {
var found = false;
for (var i = txt.length - 1; i > 0; i--) {
if (/\s|\x1B/.test(txt[i])) {
found = true;
break;
}
}
if (!found) {
if (/(https?:\/\/[^\s]+)/.test(txt))
return 0;
else
return -1;
}
var prefix = txt.substr(i + 1, 4);
if (prefix.length === 0)
return -1;
if ("http".indexOf(prefix) === 0)
return (i + 1);
};
AnsiUp.prototype.ansi_to = function (txt, formatter) {
var pkt = this._buffer + txt;
this._buffer = '';
var raw_text_pkts = pkt.split(/\x1B\[/);
if (raw_text_pkts.length === 1)
raw_text_pkts.push('');
this.handle_incomplete_sequences(raw_text_pkts);
var first_chunk = this.with_state(raw_text_pkts.shift());
var blocks = new Array(raw_text_pkts.length);
for (var i = 0, len = raw_text_pkts.length; i < len; ++i) {
blocks[i] = (formatter.transform(this.process_ansi(raw_text_pkts[i]), this));
}
if (first_chunk.text.length > 0)
blocks.unshift(formatter.transform(first_chunk, this));
return formatter.compose(blocks, this);
};
AnsiUp.prototype.ansi_to_html = function (txt) {
return this.ansi_to(txt, this.htmlFormatter);
};
AnsiUp.prototype.ansi_to_text = function (txt) {
return this.ansi_to(txt, this.textFormatter);
};
AnsiUp.prototype.with_state = function (text) {
return { bright: this.bright, fg: this.fg, bg: this.bg, text: text };
};
AnsiUp.prototype.handle_incomplete_sequences = function (chunks) {
var last_chunk = chunks[chunks.length - 1];
if ((last_chunk.length > 0) && this.detect_incomplete_ansi(last_chunk)) {
this._buffer = "\x1B[" + last_chunk;
chunks.pop();
chunks.push('');
}
else {
if (last_chunk.slice(-1) === "\x1B") {
this._buffer = "\x1B";
console.log("raw", chunks);
chunks.pop();
chunks.push(last_chunk.substr(0, last_chunk.length - 1));
console.log(chunks);
console.log(last_chunk);
}
if (chunks.length === 2 &&
chunks[1] === "" &&
chunks[0].slice(-1) === "\x1B") {
this._buffer = "\x1B";
last_chunk = chunks.shift();
chunks.unshift(last_chunk.substr(0, last_chunk.length - 1));
}
}
};
AnsiUp.prototype.process_ansi = function (block) {
if (!this._sgr_regex) {
this._sgr_regex = (_a = ["\n ^ # beginning of line\n ([!<-?]?) # a private-mode char (!, <, =, >, ?)\n ([d;]*) # any digits or semicolons\n ([ -/]? # an intermediate modifier\n [@-~]) # the command\n ([sS]*) # any text following this CSI sequence\n "], _a.raw = ["\n ^ # beginning of line\n ([!\\x3c-\\x3f]?) # a private-mode char (!, <, =, >, ?)\n ([\\d;]*) # any digits or semicolons\n ([\\x20-\\x2f]? # an intermediate modifier\n [\\x40-\\x7e]) # the command\n ([\\s\\S]*) # any text following this CSI sequence\n "], rgx(_a));
}
var matches = block.match(this._sgr_regex);
if (!matches) {
return this.with_state(block);
}
var orig_txt = matches[4];
if (matches[1] !== '' || matches[3] !== 'm') {
return this.with_state(orig_txt);
}
var sgr_cmds = matches[2].split(';');
while (sgr_cmds.length > 0) {
var sgr_cmd_str = sgr_cmds.shift();
var num = parseInt(sgr_cmd_str, 10);
if (isNaN(num) || num === 0) {
this.fg = this.bg = null;
this.bright = false;
}
else if (num === 1) {
this.bright = true;
}
else if (num === 39) {
this.fg = null;
}
else if (num === 49) {
this.bg = null;
}
else if ((num >= 30) && (num < 38)) {
var bidx = this.bright ? 1 : 0;
this.fg = this.ansi_colors[bidx][(num - 30)];
}
else if ((num >= 90) && (num < 98)) {
this.fg = this.ansi_colors[1][(num - 90)];
}
else if ((num >= 40) && (num < 48)) {
this.bg = this.ansi_colors[0][(num - 40)];
}
else if ((num >= 100) && (num < 108)) {
this.bg = this.ansi_colors[1][(num - 100)];
}
else if (num === 38 || num === 48) {
if (sgr_cmds.length > 0) {
var is_foreground = (num === 38);
var mode_cmd = sgr_cmds.shift();
if (mode_cmd === '5' && sgr_cmds.length > 0) {
var palette_index = parseInt(sgr_cmds.shift(), 10);
if (palette_index >= 0 && palette_index <= 255) {
if (is_foreground)
this.fg = this.palette_256[palette_index];
else
this.bg = this.palette_256[palette_index];
}
}
if (mode_cmd === '2' && sgr_cmds.length > 2) {
var r = parseInt(sgr_cmds.shift(), 10);
var g = parseInt(sgr_cmds.shift(), 10);
var b = parseInt(sgr_cmds.shift(), 10);
if ((r >= 0 && r <= 255) && (g >= 0 && g <= 255) && (b >= 0 && b <= 255)) {
var c = { rgb: [r, g, b], class_name: 'truecolor' };
if (is_foreground)
this.fg = c;
else
this.bg = c;
}
}
}
}
}
return this.with_state(orig_txt);
var _a;
};
return AnsiUp;
}());
//# sourceMappingURL=ansi_up.js.map
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = AnsiUp;
});

View File

@@ -29,10 +29,177 @@ var NvLineChart = React.createClass({
this.d3ChartData.datum(this.props.datum); this.d3ChartData.datum(this.props.datum);
this.d3ChartData.transition().duration(500).call(this.chart); this.d3ChartData.transition().duration(500).call(this.chart);
nv.utils.windowResize(this.chart.update); nv.utils.windowResize(this.chart.update);
},
componentWillUnmount: function() {
var nvtooltip = document.querySelector('div[class~="nvtooltip"]');
if (nvtooltip !== null) {
nvtooltip.parentElement.removeChild(nvtooltip)
}
}
});
var Tabs = React.createClass({
onTabClick: function(idx){
this.setState({ activeTab: idx });
},
/*--------------------*/
getInitialState: function(){return{
activeTab: 0
}},
render: function(){
var _this = this;
var tabsElm = [];
this.props.tabs.forEach(function(title, i){
tabsElm.push(ce('li', (i === _this.state.activeTab ? {className: 'active'} : null),
ce('a', {href: '#tab'+(i+1), onClick: _this.onTabClick.bind(_this, i)}, title)));
});
var showElement = this.props.children.map(function(child, i){
return ce('div', (i !== _this.state.activeTab ? {style: {display: 'none'}} : null), child);
});
return(ce('div', null,
ce('ul', {className: 'nav nav-tabs', id: 'tabs'}, tabsElm),
showElement
))
},
componentDidUpdate: function() {
if (this.props.stateCallback !== null) {
this.props.stateCallback(this.state.activeTab);
}
}
});
var ScrollingContent = React.createClass({
totalHeight: 0,
ownHeight: 0,
scrollRatio: 0,
lastPageY: 0,
updateScrollParams: function () {
this.totalHeight = this.refs.content.scrollHeight;
this.ownHeight = this.refs.content.clientHeight;
this.scrollRatio = this.ownHeight / this.totalHeight;
if (isNaN(this.scrollRatio)) this.scrollRatio = 0;
this.refs.scroll.style.height = this.scrollRatio * 100 + "%";
this.refs.scroll.style.top = (this.refs.content.scrollTop / this.totalHeight) * 100 + "%";
},
toggleSelect: function (value, event) { return value; },
handleScrollMouseDown: function (event) {
this.lastPageY = event.pageY;
document.body.classList.add('scroll-grabbed');
this.refs.scroll.classList.add('scroll-grabbed');
document.onselectstart = this.toggleSelect.bind(null, false);
document.addEventListener('mousemove', this.handleScrollMouseMove);
document.addEventListener('mouseup', this.handleScrollMouseUp);
},
handleScrollMouseMove: function (event) {
var delta = event.pageY - this.lastPageY;
this.lastPageY = event.pageY;
var _this = this;
window.requestAnimationFrame(function(){
_this.refs.content.scrollTop += delta / _this.scrollRatio;
_this.refs.scroll.style.top = (_this.refs.content.scrollTop / _this.totalHeight) * 100 + "%";
});
},
handleScrollMouseUp: function (event) {
document.body.classList.remove('scroll-grabbed');
this.refs.scroll.classList.remove('scroll-grabbed');
document.onselectstart = this.toggleSelect.bind(null, true);
document.removeEventListener('mousemove', this.handleScrollMouseMove);
document.removeEventListener('mouseup', this.handleScrollMouseUp);
},
/*--------------------*/
render: function () {
return (
ce('div', {className: this.props.className},
ce('div', {className: 'wrapper'},
ce('div', {className: 'content', ref: 'content'}, this.props.children)
),
ce('div', {className: 'scroll', ref: 'scroll'})
)
);
},
componentDidMount: function () {
this.updateScrollParams();
this.refs.scroll.addEventListener('mousedown', this.handleScrollMouseDown);
var _this = this;
this.refs.content.addEventListener('scroll', function(){
window.requestAnimationFrame(function(){
_this.refs.scroll.style.top = (_this.refs.content.scrollTop / _this.totalHeight) * 100 + "%";
});
});
},
componentDidUpdate: function () {
this.updateScrollParams();
}
});
var WebConsole = React.createClass({
ws: null,
connect: function(){
if (this.ws !== null) return;
var _this = this;
this.ws = new WebSocket("ws://127.0.0.1:8770"); //FIXME указывать ip:port из настроек
this.ws.onopen = function(){ console.debug('WS: open...'); };
this.ws.onclose = function(){ console.debug('WS: close...'); };
this.ws.onerror = function(e){ console.debug('WS: error'); console.error(e); };
this.ws.onmessage = function(event){
_this.setState({ lines: _this.state.lines.concat([event.data]) }); //TODO необходимо ограничить кол-во строк
};
},
disconnect: function() {
if (this.ws === null) return;
this.ws.close();
this.ws = null;
},
focusInput: function() {
this.refs.input.focus();
},
handleKeyInput: function(event) {
if (event.key === 'Enter') {
console.debug("send command '" + this.refs.input.value + "'");
this.ws.send(':'+this.refs.input.value);
this.refs.input.value = '';
}
},
/*--------------------*/
getInitialState: function(){return{
lines: []
}},
render: function(){
var ansi_up = new AnsiUp;
return(
ce('div', {id: 'webconsole'},
ce(ScrollingContent, {className: 'output'},
this.state.lines.map(function(line){
return ce('p', {dangerouslySetInnerHTML: {__html: ansi_up.ansi_to_html(line)}});
})
),
ce('input', {ref: 'input', 'onKeyPress': this.handleKeyInput})
)
)
},
componentWillUnmount: function(){
this.disconnect();
} }
}); });
var ServerInfo = React.createClass({ var ServerInfo = React.createClass({
tabStateWebConsole: function(state) {
if (state === 1) {
this.refs.webconsole.connect();
this.refs.webconsole.focusInput();
}
},
/*--------------------*/
getInitialState: function(){return { getInitialState: function(){return {
title: null, title: null,
data: [] data: []
@@ -44,12 +211,15 @@ var ServerInfo = React.createClass({
return( return(
ce('div', null, ce('div', null,
ce('h2', {style: {'margin-top': '0px'}}, this.state.title), ce('h2', {style: {'margin-top': '0px'}}, this.state.title),
ce(Tabs, {tabs: ['Онлайн', 'Консоль'], stateCallback: this.tabStateWebConsole},
ce(NvLineChart, {datum: [{ ce(NvLineChart, {datum: [{
key: 'Online players', key: 'Online players',
color: '#37d668', color: '#37d668',
area: true, area: true,
values: this.state.data values: this.state.data
}]}) }]}),
ce(WebConsole, {ref: 'webconsole'})
)
) )
) )
} }

View File

@@ -60,6 +60,13 @@ var ContentModule = React.createClass({
function(){ _this.setState({nvScriptReady: -5}); console.error('d3 - error'); } function(){ _this.setState({nvScriptReady: -5}); console.error('d3 - error'); }
); );
loadScript("/mcsmanager/ansi_up.js",
function(){ console.debug('ansi_up - ok'); },
function(){ console.debug('ansi_up - error'); }
);
loadStyle("/mcsmanager/moduleStyle.css");
this.requestServerList(); this.requestServerList();
}, },
render: function(){ render: function(){

View File

@@ -0,0 +1,57 @@
#webconsole .output {
background-color: #1e1e1e;
color: #eee;
min-height: 500px;
height: 1px;
padding: 8px;
font-family: monospace;
position: relative;
}
#webconsole .output .wrapper {
overflow: hidden;
height: 100%;
}
#webconsole .output .wrapper .content {
overflow: auto;
height: 100%;
position: relative;
right: -18px;
margin-left: -18px;
}
#webconsole .output .wrapper .content p {
margin: 0;
}
#webconsole .output .scroll {
width: 9px;
background: #f00;
position: absolute;
top: 0;
height: 21.0836%;
cursor: -webkit-grab;
cursor: -moz-grab;
right: 0;
}
.scroll-grabbed,
.scroll-grabbed * {
cursor: -webkit-grabbing !important;
cursor: -moz-grabbing !important;
}
#webconsole input {
background-color: #1e1e1e;
background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="30"><text x="7" y="24" style="font-size: 1.5em; font-family: monospace" fill="#ffffff">&gt;</text></svg>');
background-repeat: no-repeat;
color: #eee;
border: none;
padding: 8px 8px 8px 1.5em;
width: 100%;
}
#webconsole input:focus {
outline: none;
}

View File

@@ -1,5 +1,5 @@
group = 'asys' group = 'asys'
version = '0.18.3-SNAPSHOT' version = '0.18.4-SNAPSHOT'
buildscript { buildscript {
repositories { repositories {

View File

@@ -2,6 +2,7 @@
<html lang="ru"> <html lang="ru">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ASys: Web interface</title> <title>ASys: Web interface</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/yeti/bootstrap.min.css"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootswatch/3.3.7/yeti/bootstrap.min.css">
<style> <style>