MCSM -> SpacePort
This commit is contained in:
41
spaceport/build.gradle
Normal file
41
spaceport/build.gradle
Normal 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
|
||||
}
|
||||
88
spaceport/src/main/java/asys/spaceport/Activator.java
Normal file
88
spaceport/src/main/java/asys/spaceport/Activator.java
Normal 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) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
84
spaceport/src/main/java/asys/spaceport/Linker.java
Normal file
84
spaceport/src/main/java/asys/spaceport/Linker.java
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
102
spaceport/src/main/java/asys/spaceport/MCSM_WebModule.java
Normal file
102
spaceport/src/main/java/asys/spaceport/MCSM_WebModule.java
Normal 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());
|
||||
}
|
||||
}
|
||||
54
spaceport/src/main/java/asys/spaceport/ServerInfo.java
Normal file
54
spaceport/src/main/java/asys/spaceport/ServerInfo.java
Normal 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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
65
spaceport/src/main/java/asys/spaceport/server/Server.java
Normal file
65
spaceport/src/main/java/asys/spaceport/server/Server.java
Normal 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()
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
56
spaceport/src/main/java/asys/spaceport/websocket/Server.java
Normal file
56
spaceport/src/main/java/asys/spaceport/websocket/Server.java
Normal 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()
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
333
spaceport/src/main/resources/ansi_up.js
Normal file
333
spaceport/src/main/resources/ansi_up.js
Normal 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 "&";
|
||||
if (str === "<")
|
||||
return "<";
|
||||
if (str === ">")
|
||||
return ">";
|
||||
});
|
||||
};
|
||||
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;
|
||||
});
|
||||
284
spaceport/src/main/resources/components.js
Normal file
284
spaceport/src/main/resources/components.js
Normal 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)
|
||||
)}
|
||||
});
|
||||
95
spaceport/src/main/resources/module.js
Normal file
95
spaceport/src/main/resources/module.js
Normal 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)
|
||||
)
|
||||
}
|
||||
});
|
||||
90
spaceport/src/main/resources/moduleStyle.css
Normal file
90
spaceport/src/main/resources/moduleStyle.css
Normal 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">></text></svg>');
|
||||
background-repeat: no-repeat;
|
||||
color: #eee;
|
||||
border: none;
|
||||
padding: 8px 8px 8px 1.5em;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#webconsole input:focus {
|
||||
outline: none;
|
||||
}
|
||||
Reference in New Issue
Block a user