Archived
0

MCSM -> SpacePort

This commit is contained in:
2017-08-13 23:14:14 +03:00
parent f4ee3af979
commit bc2b7032a6
42 changed files with 83 additions and 84 deletions

41
spaceport/build.gradle Normal file
View File

@@ -0,0 +1,41 @@
group = 'asys'
version = '0.10.7-SNAPSHOT'
apply plugin: 'osgi'
configurations {
include
compile.extendsFrom include
}
compileJava {
dependsOn ':libprotocol:compileJava'
}
jar {
manifest {
name = 'ASys MC server manager'
instruction 'Bundle-Activator', 'asys.mcsmanager.Activator'
instruction 'Import-Package',
'!asys.mcsmanager.packets.*',
'io.netty.buffer;version="[4.0,5)"',
'io.netty.handler.codec;version="[4.0,5)"',
'io.netty.handler.codec.http;version="[4.0,5)"',
'*'
}
dependsOn configurations.include
from { configurations.include.collect { it.isDirectory() ? it : zipTree(it) } }
}
ext {
nettyVersion = '4.0.23.Final'
}
dependencies {
compile project(':core')
compile project(':webinterface')
include files(project(':libprotocol').sourceSets.main.output.classesDir)
compile group: 'io.netty', name: 'netty-codec', version: nettyVersion
compile group: 'io.netty', name: 'netty-codec-http', version: nettyVersion
}

View File

@@ -0,0 +1,88 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-03-11
*/
package asys.spaceport;
import asys.api.Config;
import asys.webinterface.api.Webinterface;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceEvent;
import org.osgi.framework.ServiceListener;
import org.osgi.util.tracker.ServiceTracker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Activator implements BundleActivator, ServiceListener {
private final Logger logger = LoggerFactory.getLogger(Activator.class);
private ServiceTracker<?, Webinterface> serviceTracker;
private MCSM_WebModule module;
private Webinterface webinterface;
private asys.spaceport.server.Server serverManager;
private asys.spaceport.websocket.Server webconsoleServer;
@Override
public void start(BundleContext context) throws Exception {
ServiceTracker<?, Config> serviceConfigTracker = new ServiceTracker<>(context, Config.class, null);
serviceConfigTracker.open();
Config config = serviceConfigTracker.getService();
if (config == null) throw new RuntimeException("Service 'Config' is not avalable!");
module = new MCSM_WebModule();
logger.debug("Get service: {}", Webinterface.class);
serviceTracker = new ServiceTracker<>(context, Webinterface.class, null);
logger.debug("Register service listener");
context.addServiceListener(this);
String host = config.getString("asys.mcsmanager.host", "127.0.0.1");
int port = config.getInt("asys.mcsmanager.port", 8779);
String passcode = config.getString("asys.mcsmanager.passcode", "testpasscode");
logger.debug("Start server manager: {}:{}", host, port);
serverManager = new asys.spaceport.server.Server();
serverManager.start(host, port, passcode);
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.spaceport.websocket.Server();
webconsoleServer.start(host, port);
serviceConfigTracker.close();
}
@Override
public void stop(BundleContext context) throws Exception {
webconsoleServer.shutdown();
serverManager.shutdown();
if (webinterface != null) {
webinterface.removeModule(module.getName());
}
context.removeServiceListener(this);
serviceTracker.close();
}
@Override
public void serviceChanged(ServiceEvent event) {
if (event.getType() == ServiceEvent.REGISTERED) {
String[] objectClass = (String[]) event.getServiceReference().getProperty("objectClass");
for (String classStr : objectClass) {
if (classStr.equals(Webinterface.class.getCanonicalName())) {
try {
serviceTracker.open();
webinterface = serviceTracker.waitForService(5000);
if (webinterface != null) {
webinterface.addModule(module);
} else {
logger.debug("service not found =(");
}
} catch (InterruptedException ignore) {
}
}
}
}
}
}

View File

@@ -0,0 +1,26 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-05-02
*/
package asys.spaceport;
import asys.spaceport.packets.CS_Ping;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
public class CSPingJsonSerializer implements JsonSerializer<CS_Ping> {
@Override
public JsonElement serialize(CS_Ping pingPacket, Type typeOfSrc, JsonSerializationContext context) {
JsonObject resultJson = new JsonObject();
resultJson.addProperty("time", pingPacket.getTime());
resultJson.addProperty("tps", pingPacket.getTps());
resultJson.addProperty("online", pingPacket.getCountPlayers());
return resultJson;
}
}

View File

@@ -0,0 +1,84 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-05-01
*/
package asys.spaceport;
import asys.spaceport.packets.CS_ConsoleMessage;
import asys.spaceport.packets.SC_Command;
import asys.spaceport.packets.SC_ToggleSendMessages;
import io.netty.channel.Channel;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import java.util.*;
public class Linker {
private static Linker instance = new Linker();
private Map<String, ServerInfo> serverMap = new HashMap<>();
private Map<String, List<Channel>> webconsoleListeners = new HashMap<>();
public static Linker getInstance() {
return instance;
}
private Linker(){
}
public void addServer(String serverName, Channel channel) {
this.serverMap.put(serverName, new ServerInfo(serverName, channel));
}
public boolean existsServer(String serverName) {
return this.serverMap.containsKey(serverName);
}
public ServerInfo getServer(String serverName) {
return this.serverMap.get(serverName);
}
public Set<String> getServerList() {
return this.serverMap.keySet();
}
public void removeServer(String serverName) {
this.serverMap.remove(serverName);
if (this.webconsoleListeners.containsKey(serverName)) {
this.webconsoleListeners.get(serverName).forEach(Channel::close);
this.webconsoleListeners.remove(serverName);
}
}
public void addWebconsoleListener(String serverName, Channel channel) {
List<Channel> channels = this.webconsoleListeners.computeIfAbsent(serverName, v -> new ArrayList<>());
if (channels.size() == 0) {
this.serverMap.get(serverName).getChannel().writeAndFlush(new SC_ToggleSendMessages(true));
}
channels.add(channel);
}
public void removeWebconsoleListener(String serverName, Channel channel) {
List<Channel> channels = this.webconsoleListeners.get(serverName);
channels.remove(channel);
if (channels.size() == 0) {
this.serverMap.get(serverName).getChannel().writeAndFlush(new SC_ToggleSendMessages(false));
}
}
public void broadcastConsoleMessage(String serverName, CS_ConsoleMessage packet) {
if (this.webconsoleListeners.containsKey(serverName)) {
this.webconsoleListeners.get(serverName).forEach(channel -> {
channel.writeAndFlush(new TextWebSocketFrame(String.format(
"[L:%d] %s",
packet.getLevel(),
packet.getMessage()
)));
});
}
}
public void sendCommand(String serverName, String command) {
if (this.serverMap.containsKey(serverName)) {
this.serverMap.get(serverName).getChannel().writeAndFlush(new SC_Command(command));
}
}
}

View File

@@ -0,0 +1,102 @@
/*
* DmitriyMX <mail@dmitriymx.ru>
* 2017-03-15
*/
package asys.spaceport;
import asys.spaceport.packets.CS_Ping;
import asys.webinterface.api.WebModule;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MCSM_WebModule extends WebModule {
private static final Gson GSON = new GsonBuilder()
.registerTypeAdapter(ServerInfo.class, new ServerInfoSerializer())
.registerTypeAdapter(CS_Ping.class, new CSPingJsonSerializer())
.create();
private final String MODULE_NAME = "mcsmanager";
private final String MODULE_URL = "/"+MODULE_NAME;
private final Pattern URL_PATTERN_JS = Pattern.compile(MODULE_URL+"/(\\w+)\\.js");
private final Pattern URL_PATTERN_CSS = Pattern.compile(MODULE_URL+"/(\\w+)\\.css");
@Override
public String getName() {
return MODULE_NAME;
}
@Override
public String getReactJSModuleLink() {
return "/"+MODULE_NAME+"/module.js";
}
@Override
public Map<String, String> getMainMenuItems() {
return new HashMap<String, String>(){{
this.put("Серверы", "/"+MODULE_NAME);
}};
}
@Override
public boolean handle(HttpExchange httpExchange) throws IOException {
String urlPath = httpExchange.getRequestURI().getPath();
if (urlPath.equals(MODULE_URL+"/servers.json")) {
return handleServersJson(httpExchange);
} else {
Matcher matcher = URL_PATTERN_JS.matcher(urlPath);
if (matcher.find()) {
InputStream stream = getClass().getResourceAsStream("/" + matcher.group(1) + ".js");
if (stream == null) {
this.sendHttpCode(httpExchange, 404, "not found");
return true;
}
httpExchange.getResponseHeaders().add("Content-Type", "text/javascript;charset=utf-8");
this.sendContent(httpExchange, 0, stream);
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;
}
private boolean handleServersJson(HttpExchange httpExchange) throws IOException {
if (httpExchange.getRequestURI().getQuery() != null &&
!httpExchange.getRequestURI().getQuery().isEmpty()) {
Map<String, String> query = this.queryToMap(httpExchange.getRequestURI().getQuery());
ServerInfo serverInfo = Linker.getInstance().getServer(query.get("clientid"));
if (serverInfo == null) {
this.sendJson(httpExchange, "{}");
} else {
this.sendJson(httpExchange, GSON.toJson(serverInfo));
}
} else {
this.sendJson(httpExchange, serverList());
}
return true;
}
private JsonElement serverList() {
return GSON.toJsonTree(Linker.getInstance().getServerList());
}
}

View File

@@ -0,0 +1,54 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-05-02
*/
package asys.spaceport;
import asys.spaceport.packets.CS_Ping;
import io.netty.channel.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Deque;
import java.util.LinkedList;
public class ServerInfo {
private final Logger logger = LoggerFactory.getLogger(ServerInfo.class);
private final String name;
private Channel channel;
private int lastOnline;
private Deque<CS_Ping> pingDeque = new LinkedList<>();
public ServerInfo(String name, Channel channel) {
this.name = name;
this.channel = channel;
}
public String getName() {
return name;
}
public Channel getChannel() {
return channel;
}
public int getLastOnline() {
return lastOnline;
}
public void setLastOnline(int lastOnline) {
this.lastOnline = lastOnline;
}
public Deque<CS_Ping> getPingDeque() {
return pingDeque;
}
public void putPing(CS_Ping ping) {
if (pingDeque.size() == 720) {
pingDeque.removeFirst();
}
pingDeque.addLast(ping);
logger.debug("putPing: {} [{}]", name, ping.getTime());
}
}

View File

@@ -0,0 +1,25 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-05-02
*/
package asys.spaceport;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import java.lang.reflect.Type;
public class ServerInfoSerializer implements JsonSerializer<ServerInfo> {
@Override
public JsonElement serialize(ServerInfo serverInfo, Type typeOfSrc, JsonSerializationContext context) {
JsonObject resultJson = new JsonObject();
resultJson.addProperty("name", serverInfo.getName());
resultJson.addProperty("lastOnline", serverInfo.getLastOnline());
resultJson.add("pingList", context.serialize(serverInfo.getPingDeque()));
return resultJson;
}
}

View File

@@ -0,0 +1,13 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-04-26
*/
package asys.spaceport.server;
import asys.spaceport.packets.SC_HandshakeResult;
abstract class HandshakeResult {
static final SC_HandshakeResult OK = new SC_HandshakeResult(0, "OK");
static final SC_HandshakeResult INVALID_PASSCODE = new SC_HandshakeResult(1, "Invalid passcode");
static final SC_HandshakeResult CLIENTID_EXISTS = new SC_HandshakeResult(2, "ClientID is already use");
}

View File

@@ -0,0 +1,65 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-04-26
*/
package asys.spaceport.server;
import asys.spaceport.packets.CS_Ping;
import asys.spaceport.packets.Packet;
import asys.spaceport.packets.codec.PacketDecoder;
import asys.spaceport.packets.codec.PacketEncoder;
import asys.spaceport.packets.codec.PacketHandler;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
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;
public class Server {
public static final BiMap<Integer, Class<? extends Packet>> knownPackets = ImmutableBiMap.of(
1, CS_Ping.class
);
private EventLoopGroup bossGroup, workerGroup;
static String passcode;
public void start(String host, int port, String passcode) {
Server.passcode = passcode;
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 PacketEncoder(),
new PacketDecoder(),
new PacketHandler(),
new ServerPacketHandler()
);
}
};
}
}

View File

@@ -0,0 +1,110 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-04-26
*/
package asys.spaceport.server;
import asys.spaceport.Linker;
import asys.spaceport.packets.*;
import com.google.common.collect.BiMap;
import com.google.common.collect.ImmutableBiMap;
import com.google.common.collect.ImmutableMap;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.AttributeKey;
import java.util.Map;
import static asys.spaceport.packets.codec.Params.KNOWN_HANDLERS;
import static asys.spaceport.packets.codec.Params.KNOWN_PACKETS;
class ServerPacketHandler extends ChannelInboundHandlerAdapter implements IPacketHandler {
private static final BiMap<Integer, Class<? extends Packet>> handshakePackets = ImmutableBiMap.of(
1, CS_Handshake.class,
2, SC_HandshakeResult.class
);
private static Map<Class<? extends Packet>, IPacketHandler> handshakeHandlers;
private static final BiMap<Integer, Class<? extends Packet>> knownPackets = ImmutableBiMap.of(
3, CS_Ping.class,
4, CS_ConsoleMessage.class,
5, SC_Command.class,
6, SC_ToggleSendMessages.class
);
private static Map<Class<? extends Packet>, IPacketHandler> pingHandlers;
private static final AttributeKey<String> CLIENTID = AttributeKey.valueOf("ClientId");
ServerPacketHandler() {
if (handshakeHandlers == null) {
handshakeHandlers = ImmutableMap.of(
CS_Handshake.class, this
);
}
if (pingHandlers == null) {
pingHandlers = ImmutableMap.of(
CS_Ping.class, this,
CS_ConsoleMessage.class, this
);
}
}
@Override
public void channelActive(ChannelHandlerContext context) throws Exception {
context.channel().attr(KNOWN_PACKETS).set(handshakePackets);
context.channel().attr(KNOWN_HANDLERS).set(handshakeHandlers);
super.channelActive(context);
}
@Override
public void channelInactive(ChannelHandlerContext context) throws Exception {
Linker.getInstance().removeServer(context.channel().attr(CLIENTID).get());
super.channelInactive(context);
}
@Override
public void handle(Packet packet, ChannelHandlerContext context) {
if (packet.getClass() == CS_Handshake.class) {
handleCSHandshake((CS_Handshake) packet, context);
} else if (packet.getClass() == CS_Ping.class) {
handleCSPing((CS_Ping) packet, context);
} else if (packet.getClass() == CS_ConsoleMessage.class) {
handleCSConsoleMessage(context.channel().attr(CLIENTID).get(), (CS_ConsoleMessage) packet);
}
}
private void handleCSHandshake(CS_Handshake packet, ChannelHandlerContext context) {
if (!packet.getPasscode().equalsIgnoreCase(Server.passcode)) {
try {
context.channel().writeAndFlush(HandshakeResult.INVALID_PASSCODE).sync().channel().close();
} catch (InterruptedException ignore) {
}
return;
}
if (Linker.getInstance().existsServer(packet.getClientId())) {
try {
context.channel().writeAndFlush(HandshakeResult.CLIENTID_EXISTS).sync().channel().close();
} catch (InterruptedException ignore) {
}
return;
} else {
Linker.getInstance().addServer(packet.getClientId(), context.channel());
}
context.channel().write(HandshakeResult.OK);
context.channel().attr(CLIENTID).set(packet.getClientId());
context.channel().attr(KNOWN_PACKETS).set(knownPackets);
context.channel().attr(KNOWN_HANDLERS).set(pingHandlers);
context.channel().flush();
}
private void handleCSPing(CS_Ping packet, ChannelHandlerContext context) {
Linker.getInstance().getServer(context.channel().attr(CLIENTID).get()).putPing(packet);
}
private void handleCSConsoleMessage(String serverName, CS_ConsoleMessage packet) {
Linker.getInstance().broadcastConsoleMessage(serverName, packet);
}
}

View File

@@ -0,0 +1,41 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-05-09
*/
package asys.spaceport.websocket;
import asys.spaceport.Linker;
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 io.netty.util.AttributeKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class FrameHandler extends SimpleChannelInboundHandler<WebSocketFrame> {
private static final AttributeKey<String> WC_SERVERNAME = AttributeKey.valueOf("WC_SERVERNAME");
private final Logger logger = LoggerFactory.getLogger(FrameHandler.class);
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
Linker.getInstance().removeWebconsoleListener(ctx.channel().attr(WC_SERVERNAME).get(), ctx.channel());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, WebSocketFrame frame) throws Exception {
if (frame instanceof TextWebSocketFrame) {
String requestText = ((TextWebSocketFrame)frame).text();
if (requestText.startsWith("]")) {
String serverName = requestText.substring(1);
ctx.channel().attr(WC_SERVERNAME).set(serverName);
Linker.getInstance().addWebconsoleListener(serverName, ctx.channel());
} else if (requestText.startsWith(":")) {
String command = requestText.substring(1);
Linker.getInstance().sendCommand(ctx.channel().attr(WC_SERVERNAME).get(), command);
}
} else {
logger.warn("unsupport frame type: {}", frame.getClass().getName());
}
}
}

View File

@@ -0,0 +1,56 @@
/*
* DmitriyMX <dimon550@gmail.com>
* 2017-05-08
*/
package asys.spaceport.websocket;
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 {
private EventLoopGroup bossGroup, workerGroup;
public void start(String host, int port) {
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

@@ -0,0 +1,284 @@
/**
* График.
* <code>this.props.datum</code> - данные в формате D3
*/
var NvLineChart = React.createClass({
chart: null,
d3ChartData: null,
/*--------------------*/
render: function(){return(
ce('div', {id: 'chart'}, ce('svg', {style: {'height': '500px'}}))
)},
componentDidMount: function() {
var _this = this;
nv.addGraph(function() {
_this.chart = nv.models.lineChart().useInteractiveGuideline(true);
_this.chart.xAxis.axisLabel('Time').tickFormat(function(d){ return d3.time.format('%X')(new Date(d)); });
_this.chart.yAxis.axisLabel('Players').tickFormat(d3.format('d'));
_this.d3ChartData = d3.select('#chart svg').datum(_this.props.datum);
_this.d3ChartData.transition().duration(500).call(_this.chart);
nv.utils.windowResize(_this.chart.update);
return _this.chart;
});
},
componentDidUpdate: function() {
if (this.d3ChartData === null) return;
this.d3ChartData.datum(this.props.datum);
this.d3ChartData.transition().duration(500).call(this.chart);
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,
autoScroll: true,
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;
var h = this.scrollRatio * 100;
if (h < 100) {
if (this.autoScroll) {
this.refs.content.scrollTop = this.totalHeight;
}
this.refs.scroll.style.height = h + "%";
this.refs.scroll.style.top = (this.refs.content.scrollTop / this.totalHeight) * 100 + "%";
}
},
toggleSelect: function (value, event) { return value; },
handleMouseDown: 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.handleMouseMove);
document.addEventListener('mouseup', this.handleMouseUp);
},
handleMouseMove: 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 + "%";
});
},
handleMouseUp: 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.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
},
handleScroll: function (event) {
var zH = this.refs.gencon.offsetHeight;
var bottomPoint = ((this.refs.content.scrollTop / this.totalHeight) * zH) +
(this.scrollRatio * zH);
this.autoScroll = (bottomPoint === zH);
},
/*--------------------*/
render: function () {
return (
ce('div', {className: this.props.className, ref: 'gencon'},
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.handleMouseDown);
this.refs.content.addEventListener('scroll', this.handleScroll);
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(serverName){
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.send(']'+serverName);
};
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){
var clazz = "";
if (line.indexOf('ERROR') !== -1) { clazz = "error"; }
else if (line.indexOf('WARN') !== -1) { clazz = "warn"; }
return ce('p', {className: clazz, dangerouslySetInnerHTML: {__html: ansi_up.ansi_to_html(line)}});
})
),
ce('input', {ref: 'input', 'onKeyPress': this.handleKeyInput})
)
)
},
componentWillUnmount: function(){
this.disconnect();
}
});
var ServerInfo = React.createClass({
tabStateWebConsole: function(state) {
if (state === 1) {
this.refs.webconsole.connect(this.state.title);
this.refs.webconsole.focusInput();
}
},
/*--------------------*/
getInitialState: function(){return {
title: null,
data: []
}},
render: function(){
if (this.state.title === null) {
return ce('div', null, 'no data');
} else {
return(
ce('div', null,
ce('h2', {style: {'margin-top': '0px'}}, this.state.title),
ce(Tabs, {tabs: ['Онлайн', 'Консоль'], stateCallback: this.tabStateWebConsole, ref: 'tabs'},
ce(NvLineChart, {datum: [{
key: 'Online players',
color: '#37d668',
area: true,
values: this.state.data
}]}),
ce(WebConsole, {ref: 'webconsole'})
)
)
)
}
},
componentWillUpdate: function(nextProps, nextState) {
if (this.state.title === null) return;
if (this.state.title !== nextState.title) {
this.refs.webconsole.disconnect();
this.refs.webconsole.setState({lines: []});
this.refs.tabs.setState({activeTab: 0});
}
}
});
/**
* Пункт списка серверов.
* <code>this.props.title</code> - заголовок / clientId
* <code>this.props.onClick</code> - callback события клика
* <code>this.props.active</code> - состояние активности (true/false)
*/
var ServerListItem = React.createClass({
render: function(){
return(
ce('a', {className: 'list-group-item clearfix' + (this.props.active ? ' active' : ''), href: '#',
onClick: this.props.onClick},
ce('span', {style: {'padding-top': '15px'}},
ce('span', {className: 'glyphicon glyphicon-tasks'}),
nbsp,
this.props.title
)
)
)
}
});
var ServerList = React.createClass({
render: function(){return(
ce('div', {className: 'list-group'}, this.props.children)
)}
});

View File

@@ -0,0 +1,95 @@
var ContentModule = React.createClass({
requestServerList: function() {
var _this = this;
fetch('/mcsmanager/servers.json')
.then(function(response) {
response.json().then(function(data) {
console.debug(data);
_this.setState({serverList: data})
});
})
.catch(function(err){
console.error(err);
});
},
convertToChart: function(pingList) {
var resultArray = [];
pingList.forEach(function(pingData){
resultArray.push({x: pingData.time, y: pingData.online})
});
return resultArray;
},
clickServerListItem: function(title) {
var _this = this;
fetch('/mcsmanager/servers.json?clientid='+title)
.then(function(response){
response.json().then(function(data){
console.debug(data);
_this.refs.serverInfo.setState({
title: data.name,
data: _this.convertToChart(data.pingList)
});
});
})
.catch(function(err){
console.error(err);
});
},
/*--------------------*/
getInitialState: function(){return{
nvScriptReady: 0,
serverList: []
}},
componentWillMount: function(){
var _this = this;
loadScript("/mcsmanager/components.js",
function(){ _this.setState({nvScriptReady: _this.state.nvScriptReady+1}); console.debug('components - ok'); },
function(){ _this.setState({nvScriptReady: -5}); console.debug('components - error'); }
);
loadScript("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js",
function(){
_this.setState({nvScriptReady: _this.state.nvScriptReady+1}); console.info('d3 - ok');
loadStyle("https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.5/nv.d3.min.css");
loadScript("https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.5/nv.d3.js",
function(){ _this.setState({nvScriptReady: _this.state.nvScriptReady+1}); console.info('nv - ok'); },
function(){ _this.setState({nvScriptReady: -5}); console.error('nv - 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();
},
render: function(){
var element;
if (this.state.nvScriptReady === 3) {
var _this = this;
var serverListItems = [];
this.state.serverList.forEach(function(item){
serverListItems.push(ce(ServerListItem, {title: item, onClick: _this.clickServerListItem.bind(_this, item)}));
});
element = ce('div', {className: 'row'},
ce('div', {className: 'col-md-3'}, ce(ServerList, null, serverListItems)),
ce('div', {className: 'col-md-9'}, ce(ServerInfo, {ref: "serverInfo"}))
);
} else if (this.state.nvScriptReady < 0) {
element = ce('span', null, 'error');
} else {
element = ce('span', null, 'loading...');
}
return(
ce(Panel, {title: 'Серверы'}, element)
)
}
});

View File

@@ -0,0 +1,90 @@
#webconsole .output {
background-color: #1e1e1e;
color: #eee;
min-height: 500px;
height: 1px;
padding: 0 8px 0 8px;
font-family: monospace;
position: relative;
}
#webconsole .output .wrapper {
overflow: hidden;
height: 100%;
}
#webconsole .output .wrapper::before {
content: '';
position: absolute;
z-index: 1;
top: 0;
left: 0;
right: 0;
height: 5%;
background: linear-gradient(#1e1e1e 0%, rgba(30,30,30,0) 100%);
}
#webconsole .output .wrapper::after {
content: '';
position: absolute;
z-index: 1;
bottom: 0;
left: 0;
right: 0;
height: 5%;
background: linear-gradient(rgba(30,30,30,0) 0%, #1e1e1e 100%);
}
#webconsole .output .wrapper .content {
overflow: auto;
height: 100%;
position: relative;
right: -18px;
margin-left: -18px;
padding: 8px 0 8px 0;
}
#webconsole .output .wrapper .content p {
margin: 0;
word-wrap: break-word;
}
#webconsole .output .wrapper .content p.error {
background-color: rgba(255,0,0,0.4);
}
#webconsole .output .wrapper .content p.warn {
background-color: rgba(255,200,0,0.3);
}
#webconsole .output .scroll {
width: 9px;
background: #f00;
position: absolute;
top: 0;
height: 21.0836%;
cursor: -webkit-grab;
cursor: -moz-grab;
right: 0;
z-index: 1;
}
.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;
}