Webmine
BIN
src/main/angular/public/webmine/block/activator/blue_false.png
Normal file
|
After Width: | Height: | Size: 310 B |
BIN
src/main/angular/public/webmine/block/activator/blue_null.png
Normal file
|
After Width: | Height: | Size: 370 B |
BIN
src/main/angular/public/webmine/block/activator/blue_true.png
Normal file
|
After Width: | Height: | Size: 300 B |
BIN
src/main/angular/public/webmine/block/activator/cyan_false.png
Normal file
|
After Width: | Height: | Size: 311 B |
BIN
src/main/angular/public/webmine/block/activator/cyan_null.png
Normal file
|
After Width: | Height: | Size: 364 B |
BIN
src/main/angular/public/webmine/block/activator/cyan_true.png
Normal file
|
After Width: | Height: | Size: 299 B |
BIN
src/main/angular/public/webmine/block/activator/green_false.png
Normal file
|
After Width: | Height: | Size: 310 B |
BIN
src/main/angular/public/webmine/block/activator/green_null.png
Normal file
|
After Width: | Height: | Size: 368 B |
BIN
src/main/angular/public/webmine/block/activator/green_true.png
Normal file
|
After Width: | Height: | Size: 302 B |
|
After Width: | Height: | Size: 311 B |
BIN
src/main/angular/public/webmine/block/activator/magenta_null.png
Normal file
|
After Width: | Height: | Size: 373 B |
BIN
src/main/angular/public/webmine/block/activator/magenta_true.png
Normal file
|
After Width: | Height: | Size: 313 B |
BIN
src/main/angular/public/webmine/block/activator/red_false.png
Normal file
|
After Width: | Height: | Size: 311 B |
BIN
src/main/angular/public/webmine/block/activator/red_null.png
Normal file
|
After Width: | Height: | Size: 371 B |
BIN
src/main/angular/public/webmine/block/activator/red_true.png
Normal file
|
After Width: | Height: | Size: 317 B |
BIN
src/main/angular/public/webmine/block/activator/yellow_false.png
Normal file
|
After Width: | Height: | Size: 311 B |
BIN
src/main/angular/public/webmine/block/activator/yellow_null.png
Normal file
|
After Width: | Height: | Size: 373 B |
BIN
src/main/angular/public/webmine/block/activator/yellow_true.png
Normal file
|
After Width: | Height: | Size: 316 B |
BIN
src/main/angular/public/webmine/block/observer/blue_false.png
Normal file
|
After Width: | Height: | Size: 268 B |
BIN
src/main/angular/public/webmine/block/observer/blue_null.png
Normal file
|
After Width: | Height: | Size: 349 B |
BIN
src/main/angular/public/webmine/block/observer/blue_true.png
Normal file
|
After Width: | Height: | Size: 257 B |
BIN
src/main/angular/public/webmine/block/observer/cyan_false.png
Normal file
|
After Width: | Height: | Size: 266 B |
BIN
src/main/angular/public/webmine/block/observer/cyan_null.png
Normal file
|
After Width: | Height: | Size: 343 B |
BIN
src/main/angular/public/webmine/block/observer/cyan_true.png
Normal file
|
After Width: | Height: | Size: 255 B |
BIN
src/main/angular/public/webmine/block/observer/green_false.png
Normal file
|
After Width: | Height: | Size: 270 B |
BIN
src/main/angular/public/webmine/block/observer/green_null.png
Normal file
|
After Width: | Height: | Size: 347 B |
BIN
src/main/angular/public/webmine/block/observer/green_true.png
Normal file
|
After Width: | Height: | Size: 253 B |
BIN
src/main/angular/public/webmine/block/observer/magenta_false.png
Normal file
|
After Width: | Height: | Size: 267 B |
BIN
src/main/angular/public/webmine/block/observer/magenta_null.png
Normal file
|
After Width: | Height: | Size: 345 B |
BIN
src/main/angular/public/webmine/block/observer/magenta_true.png
Normal file
|
After Width: | Height: | Size: 270 B |
BIN
src/main/angular/public/webmine/block/observer/red_false.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
src/main/angular/public/webmine/block/observer/red_null.png
Normal file
|
After Width: | Height: | Size: 343 B |
BIN
src/main/angular/public/webmine/block/observer/red_true.png
Normal file
|
After Width: | Height: | Size: 265 B |
BIN
src/main/angular/public/webmine/block/observer/yellow_false.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
src/main/angular/public/webmine/block/observer/yellow_null.png
Normal file
|
After Width: | Height: | Size: 351 B |
BIN
src/main/angular/public/webmine/block/observer/yellow_true.png
Normal file
|
After Width: | Height: | Size: 266 B |
@ -1,5 +1,5 @@
|
|||||||
import {Routes} from '@angular/router';
|
import {Routes} from '@angular/router';
|
||||||
import {ServerListComponent} from './server-list/server-list.component';
|
import {ServerListComponent} from './server/list/server-list.component';
|
||||||
|
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
{path: '**', component: ServerListComponent},
|
{path: '**', component: ServerListComponent},
|
||||||
|
|||||||
@ -2,39 +2,61 @@ export type FromJson<T> = (json: any) => T;
|
|||||||
|
|
||||||
export type Next<T> = (t: T) => any;
|
export type Next<T> = (t: T) => any;
|
||||||
|
|
||||||
export function validateString(json: any) {
|
|
||||||
if (typeof json === 'string') {
|
|
||||||
return json;
|
|
||||||
}
|
|
||||||
throw new Error('Not a string: ' + JSON.stringify(json));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function orNull<T, R>(t: T | null, map: (t: T) => R): R | null {
|
export function orNull<T, R>(t: T | null, map: (t: T) => R): R | null {
|
||||||
if (t === null) {
|
if (t === null || t === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return map(t);
|
return map(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function orElse<T, R>(t: T | null, map: (t: T) => R, orElse: R): R {
|
export function orElse<T, R>(t: T | null | undefined, map: (t: T) => R, orElse: R): R {
|
||||||
if (t === null) {
|
if (t === null || t === undefined) {
|
||||||
return orElse;
|
return orElse;
|
||||||
}
|
}
|
||||||
return map(t);
|
return map(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateBoolean(json: any) {
|
export function validateBoolean(json: any) {
|
||||||
if (typeof json === 'boolean') {
|
if (typeof json !== 'boolean') {
|
||||||
return json;
|
throw new Error(`Not a Boolean: type=${typeof json}, value=${JSON.stringify(json)}`);
|
||||||
}
|
}
|
||||||
throw new Error('Not a boolean: ' + JSON.stringify(json));
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateNumber(json: any) {
|
export function validateNumber(json: any) {
|
||||||
if (typeof json === 'number') {
|
if (typeof json !== 'number') {
|
||||||
|
throw new Error(`Not a Number: type=${typeof json}, value=${JSON.stringify(json)}`);
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
throw new Error('Not a number: ' + JSON.stringify(json));
|
|
||||||
|
export function validateString(json: any) {
|
||||||
|
if (typeof json !== 'string') {
|
||||||
|
throw new Error(`Not a String: type=${typeof json}, value=${JSON.stringify(json)}`);
|
||||||
|
}
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateList<T>(json: any, fromJson: FromJson<T>): T[] {
|
||||||
|
if (!Array.isArray(json)) {
|
||||||
|
throw new Error(`Not a list: type=${typeof json}, value=${JSON.stringify(json)}`);
|
||||||
|
}
|
||||||
|
return json.map(fromJson);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateMap<K, V>(json: any, key: FromJson<K>, value: FromJson<V>): Map<K, V> {
|
||||||
|
if (typeof json !== 'object') {
|
||||||
|
throw new Error(`Not a Map: type=${typeof json}, value=${JSON.stringify(json)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = new Map<K, V>();
|
||||||
|
for (const [rawKey, rawValue] of Object.entries(json)) {
|
||||||
|
const parsedKey = key(rawKey);
|
||||||
|
const parsedValue = value(rawValue);
|
||||||
|
result.set(parsedKey, parsedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function url(protocol: string, path: any[]): string {
|
export function url(protocol: string, path: any[]): string {
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
import {Mode} from "./Mode";
|
|
||||||
import {validateBoolean, validateNumber, validateString} from "../crud/CrudHelpers";
|
|
||||||
|
|
||||||
export class Server {
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
readonly name: string,
|
|
||||||
readonly motd: string,
|
|
||||||
readonly mode: Mode,
|
|
||||||
readonly port: number,
|
|
||||||
readonly running: boolean,
|
|
||||||
readonly icon: boolean,
|
|
||||||
) {
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
static fromJson(json: any): Server {
|
|
||||||
return new Server(
|
|
||||||
validateString(json.name),
|
|
||||||
validateString(json.motd),
|
|
||||||
validateString(json.mode) as Mode,
|
|
||||||
validateNumber(json.port),
|
|
||||||
validateBoolean(json.running),
|
|
||||||
validateBoolean(json.icon),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
<div class="heading">
|
|
||||||
<img src="minecraft.svg" alt="Minecraft">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="list">
|
|
||||||
<div *ngFor="let server of servers()" class="server">
|
|
||||||
<div class="icon">
|
|
||||||
<img src="{{server.mode.toLowerCase()}}.png" alt="{{server.mode}}" *ngIf="!server.icon">
|
|
||||||
<img [src]="url('http',['Server', server.name, 'icon'])" alt="{{server.mode}}" *ngIf="server.icon">
|
|
||||||
</div>
|
|
||||||
<div class="name">
|
|
||||||
{{ server.motd }}
|
|
||||||
<div class="address">
|
|
||||||
10.255.0.1:{{ server.port }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="map" *ngIf="server.name === 'survival24'">
|
|
||||||
<a href="https://mc.ph87.de/" target="_blank">
|
|
||||||
<img src="map.svg" alt="Karte">
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="command start" [class.startInactive]="!server.running" [class.startActive]="server.running" (click)="start(server)">
|
|
||||||
An
|
|
||||||
</div>
|
|
||||||
<div class="command stop" [class.stopInactive]="server.running" [class.stopActive]="!server.running" (click)="stop(server)">
|
|
||||||
Aus
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="hint">
|
|
||||||
<img src="info.svg" alt="(i)">
|
|
||||||
Beim Starten eines Servers werden alle anderen gestoppt.
|
|
||||||
</div>
|
|
||||||
73
src/main/angular/src/app/server/Server.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import {Mode} from "./Mode";
|
||||||
|
import {orElse, validateBoolean, validateList, validateNumber, validateString} from "../crud/CrudHelpers";
|
||||||
|
|
||||||
|
export enum Channel {
|
||||||
|
red = 'red',
|
||||||
|
blue = 'blue',
|
||||||
|
green = 'green',
|
||||||
|
yellow = 'yellow',
|
||||||
|
magenta = 'magenta',
|
||||||
|
cyan = 'cyan',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateChannel(json: any): Channel {
|
||||||
|
return validateString(json) as Channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ChannelInstance {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly channel: Channel,
|
||||||
|
readonly signal: number,
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): ChannelInstance {
|
||||||
|
return new ChannelInstance(
|
||||||
|
validateChannel(json.channel),
|
||||||
|
validateNumber(json.signal),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Server {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly name: string,
|
||||||
|
readonly motd: string,
|
||||||
|
readonly mode: Mode,
|
||||||
|
readonly port: number,
|
||||||
|
readonly running: boolean,
|
||||||
|
readonly webmine: boolean,
|
||||||
|
readonly icon: boolean,
|
||||||
|
readonly activators: ChannelInstance[],
|
||||||
|
readonly observers: ChannelInstance[],
|
||||||
|
) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
static fromJson(json: any): Server {
|
||||||
|
return new Server(
|
||||||
|
validateString(json.name),
|
||||||
|
validateString(json.motd),
|
||||||
|
validateString(json.mode) as Mode,
|
||||||
|
validateNumber(json.port),
|
||||||
|
validateBoolean(json.running),
|
||||||
|
validateBoolean(json.webmine),
|
||||||
|
validateBoolean(json.icon),
|
||||||
|
validateList(json['activators'], ChannelInstance.fromJson),
|
||||||
|
validateList(json['observers'], ChannelInstance.fromJson),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
isActivatorPowered(channel: Channel): boolean | null {
|
||||||
|
return orElse(this.activators.filter(c => c.channel === channel).map(c => c.signal > 0)[0], v => v, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
isObserverPowered(channel: Channel): boolean | null {
|
||||||
|
return orElse(this.observers.filter(c => c.channel === channel).map(c => c.signal > 0)[0], v => v, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
<div class="heading">
|
||||||
|
<img src="minecraft.svg" alt="Minecraft">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="list">
|
||||||
|
<div *ngFor="let server of servers()" class="server">
|
||||||
|
<div class="icon">
|
||||||
|
<img src="{{server.mode.toLowerCase()}}.png" alt="{{server.mode}}" *ngIf="!server.icon">
|
||||||
|
<img [src]="url('http',['Server', server.name, 'icon'])" alt="{{server.mode}}" *ngIf="server.icon">
|
||||||
|
</div>
|
||||||
|
<div class="name">
|
||||||
|
{{ server.motd }}
|
||||||
|
<div class="address">
|
||||||
|
10.255.0.1:{{ server.port }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="map" *ngIf="server.name === 'survival24'">
|
||||||
|
<a href="https://mc.ph87.de/" target="_blank">
|
||||||
|
<img src="map.svg" alt="Karte">
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="command start" [class.startInactive]="!server.running" [class.startActive]="server.running" (click)="start(server)">
|
||||||
|
An
|
||||||
|
</div>
|
||||||
|
<div class="command stop" [class.stopInactive]="server.running" [class.stopActive]="!server.running" (click)="stop(server)">
|
||||||
|
Aus
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ng-container *ngIf="server.running && server.webmine">
|
||||||
|
<div class="channelInstanceList">
|
||||||
|
<div class="channelInstance" *ngFor="let channel of channels()">
|
||||||
|
<img src="webmine/block/activator/{{channel}}_null.png" [class.hidden]="server.isActivatorPowered(channel) !== null" alt="{{channel}}" (click)="crudService.webmineActivatorToggle(server, channel)">
|
||||||
|
<img src="webmine/block/activator/{{channel}}_false.png" [class.hidden]="server.isActivatorPowered(channel) !== false" alt="{{channel}}" (click)="crudService.webmineActivatorToggle(server, channel)">
|
||||||
|
<img src="webmine/block/activator/{{channel}}_true.png" [class.hidden]="server.isActivatorPowered(channel) !== true" alt="{{channel}}" (click)="crudService.webmineActivatorToggle(server, channel)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="channelInstanceList">
|
||||||
|
<div class="channelInstance" *ngFor="let channel of channels()">
|
||||||
|
<img src="webmine/block/observer/{{channel}}_null.png" [class.hidden]="server.isObserverPowered(channel) !== null" alt="{{channel}}">
|
||||||
|
<img src="webmine/block/observer/{{channel}}_false.png" [class.hidden]="server.isObserverPowered(channel) !== false" alt="{{channel}}">
|
||||||
|
<img src="webmine/block/observer/{{channel}}_true.png" [class.hidden]="server.isObserverPowered(channel) !== true" alt="{{channel}}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="message messageError" *ngIf="!crudService.api.connected">
|
||||||
|
<img src="error.svg" alt="(x)">
|
||||||
|
Nicht verbunden!
|
||||||
|
</div>
|
||||||
@ -3,16 +3,22 @@
|
|||||||
|
|
||||||
.server {
|
.server {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #464d55;
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
padding: 0.25em;
|
padding: 0.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
img {
|
|
||||||
height: 2em;
|
height: 2em;
|
||||||
|
width: 2em;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -25,6 +31,7 @@
|
|||||||
|
|
||||||
.map {
|
.map {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
height: 2em;
|
height: 2em;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
@ -61,6 +68,23 @@
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.channelInstanceList {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
|
||||||
|
.channelInstance {
|
||||||
|
display: block;
|
||||||
|
width: calc(100% / 6);
|
||||||
|
padding-left: 0.25em;
|
||||||
|
padding-right: 0.25em;
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,9 +1,9 @@
|
|||||||
import {Component} from '@angular/core';
|
import {Component} from '@angular/core';
|
||||||
import {NgForOf, NgIf} from '@angular/common';
|
import {NgForOf, NgIf} from '@angular/common';
|
||||||
import {Server} from './Server';
|
import {Channel, Server} from '../Server';
|
||||||
import {CrudListComponent} from '../crud/CrudListComponent';
|
import {CrudListComponent} from '../../crud/CrudListComponent';
|
||||||
import {ServerService} from './server.service';
|
import {ServerService} from '../server.service';
|
||||||
import {url} from '../crud/CrudHelpers';
|
import {url} from '../../crud/CrudHelpers';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-server-list',
|
selector: 'app-server-list',
|
||||||
@ -36,4 +36,8 @@ export class ServerListComponent extends CrudListComponent<Server, ServerService
|
|||||||
return this.list.sort((a, b) => a.port - b.port);
|
return this.list.sort((a, b) => a.port - b.port);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
channels(): Channel[] {
|
||||||
|
return Object.keys(Channel).filter(key => isNaN(Number(key))).map(key => key as Channel);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import {Injectable} from '@angular/core';
|
import {Injectable} from '@angular/core';
|
||||||
import {CrudService} from '../crud/CrudService';
|
import {CrudService} from '../crud/CrudService';
|
||||||
import {Server} from './Server';
|
import {Channel, Server} from './Server';
|
||||||
import {ApiService} from '../crud/ApiService';
|
import {ApiService} from '../crud/ApiService';
|
||||||
import {Next} from '../crud/CrudHelpers';
|
import {Next} from '../crud/CrudHelpers';
|
||||||
|
|
||||||
@ -23,4 +23,8 @@ export class ServerService extends CrudService<Server> {
|
|||||||
this.getSingle([server.name, 'stop'], next);
|
this.getSingle([server.name, 'stop'], next);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
webmineActivatorToggle(server: Server, channel: Channel) {
|
||||||
|
this.getNone(['Activator', server.name, channel, server.isActivatorPowered(channel) === true ? 0 : 15])
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -33,3 +33,7 @@ body {
|
|||||||
background-color: #fdaaaa;
|
background-color: #fdaaaa;
|
||||||
border: 0.1em solid red;
|
border: 0.1em solid red;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|||||||
22
src/main/java/de/ph87/mc/server/ChannelInstanceDto.java
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package de.ph87.mc.server;
|
||||||
|
|
||||||
|
import de.ph87.mc.webmine.Channel;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ChannelInstanceDto {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final Channel channel;
|
||||||
|
|
||||||
|
public final int signal;
|
||||||
|
|
||||||
|
public ChannelInstanceDto(@NonNull final Map.Entry<Channel, Integer> entry) {
|
||||||
|
this.channel = entry.getKey();
|
||||||
|
this.signal = entry.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
8
src/main/java/de/ph87/mc/server/EConsumer.java
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package de.ph87.mc.server;
|
||||||
|
|
||||||
|
@FunctionalInterface
|
||||||
|
public interface EConsumer<T, E extends Exception> {
|
||||||
|
|
||||||
|
void accept(final T t) throws E;
|
||||||
|
|
||||||
|
}
|
||||||
12
src/main/java/de/ph87/mc/server/InvalidChannelSignal.java
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package de.ph87.mc.server;
|
||||||
|
|
||||||
|
import de.ph87.mc.webmine.Channel;
|
||||||
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
public class InvalidChannelSignal extends Exception {
|
||||||
|
|
||||||
|
public InvalidChannelSignal(@NonNull final Server server, @NonNull final Channel channel, final int signal) {
|
||||||
|
super("Invalid signal received: server=%s, channel=%s, signal=%s".formatted(server.properties.name, channel, signal));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -31,7 +31,7 @@ public class Properties {
|
|||||||
final java.util.Properties properties = new java.util.Properties();
|
final java.util.Properties properties = new java.util.Properties();
|
||||||
try (final FileReader reader = new FileReader(file)) {
|
try (final FileReader reader = new FileReader(file)) {
|
||||||
properties.load(reader);
|
properties.load(reader);
|
||||||
name = properties.getProperty("level-name");
|
name = directory.getName();
|
||||||
motd = properties.getProperty("motd");
|
motd = properties.getProperty("motd");
|
||||||
mode = Mode.valueOf(properties.getProperty("gamemode").toUpperCase(Locale.ROOT));
|
mode = Mode.valueOf(properties.getProperty("gamemode").toUpperCase(Locale.ROOT));
|
||||||
port = Integer.parseInt(properties.getProperty("server-port"));
|
port = Integer.parseInt(properties.getProperty("server-port"));
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
package de.ph87.mc.server;
|
package de.ph87.mc.server;
|
||||||
|
|
||||||
|
import de.ph87.mc.webmine.Channel;
|
||||||
import jakarta.annotation.Nullable;
|
import jakarta.annotation.Nullable;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.net.URLConnection;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@ -30,6 +36,12 @@ public class Server {
|
|||||||
|
|
||||||
public boolean shutdown = false;
|
public boolean shutdown = false;
|
||||||
|
|
||||||
|
public boolean webmine = false;
|
||||||
|
|
||||||
|
private final Map<Channel, Integer> activators = new HashMap<>();
|
||||||
|
|
||||||
|
private final Map<Channel, Integer> observers = new HashMap<>();
|
||||||
|
|
||||||
public Server(@NonNull final File directory) throws NoMinecraftServer {
|
public Server(@NonNull final File directory) throws NoMinecraftServer {
|
||||||
this.directory = directory;
|
this.directory = directory;
|
||||||
this.pidFile = new File(directory, "pid");
|
this.pidFile = new File(directory, "pid");
|
||||||
@ -37,19 +49,47 @@ public class Server {
|
|||||||
this.properties = new Properties(directory);
|
this.properties = new Properties(directory);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRunning() {
|
|
||||||
synchronized (lock) {
|
|
||||||
return process != null && process.isAlive();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return "Server(%s, \"%s\", %s)".formatted(properties.mode, properties.motd, isRunning() ? "RUNNING" : "stopped");
|
return "Server(%s, \"%s\", %s)".formatted(properties.mode, properties.motd, process != null ? "RUNNING" : "stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean eq(@NonNull final Server other) {
|
public boolean eq(@NonNull final Server other) {
|
||||||
return properties.name.equals(other.properties.name);
|
return properties.name.equals(other.properties.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setActivator(final Channel channel, final int signal) throws InvalidChannelSignal {
|
||||||
|
if (signal < 0 || signal > 15) {
|
||||||
|
throw new InvalidChannelSignal(this, channel, signal);
|
||||||
|
}
|
||||||
|
synchronized (activators) {
|
||||||
|
activators.put(channel, signal);
|
||||||
|
publish(channel, signal);
|
||||||
|
log.info("Activator changed: name={}, channel={}, signal={}", properties.name, channel, signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setObserver(final Channel channel, final int signal) throws InvalidChannelSignal {
|
||||||
|
if (signal < 0 || signal > 15) {
|
||||||
|
throw new InvalidChannelSignal(this, channel, signal);
|
||||||
|
}
|
||||||
|
synchronized (observers) {
|
||||||
|
observers.put(channel, signal);
|
||||||
|
log.info("Observer changed: name={}: {}={}", properties.name, channel, signal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void publish(@NonNull final Channel channel, final int signal) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
final URLConnection con = URI.create("http://localhost:8123/set?channel=%s&signal=%d".formatted(channel, signal)).toURL().openConnection();
|
||||||
|
con.setConnectTimeout(500);
|
||||||
|
con.setReadTimeout(500);
|
||||||
|
con.connect();
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("Failed to publish Activator to Server: name={}, channel={}, signal={}, error={}", properties.name, channel, signal, e.getMessage());
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,28 +4,44 @@ import de.ph87.mc.websocket.IWebsocketMessage;
|
|||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
public class ServerDto implements IWebsocketMessage {
|
public class ServerDto implements IWebsocketMessage {
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public final String name;
|
public final String name;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public final String motd;
|
public final String motd;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public final Mode mode;
|
public final Mode mode;
|
||||||
|
|
||||||
public final int port;
|
public final int port;
|
||||||
|
|
||||||
public final boolean running;
|
public final boolean running;
|
||||||
|
|
||||||
|
public final boolean webmine;
|
||||||
|
|
||||||
public boolean icon;
|
public boolean icon;
|
||||||
|
|
||||||
public ServerDto(final @NonNull Server server) {
|
@NonNull
|
||||||
|
public final List<ChannelInstanceDto> activators;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public final List<ChannelInstanceDto> observers;
|
||||||
|
|
||||||
|
public ServerDto(@NonNull final Server server) {
|
||||||
this.name = server.properties.name;
|
this.name = server.properties.name;
|
||||||
this.motd = server.properties.motd;
|
this.motd = server.properties.motd;
|
||||||
this.mode = server.properties.mode;
|
this.mode = server.properties.mode;
|
||||||
this.port = server.properties.port;
|
this.port = server.properties.port;
|
||||||
this.running = server.isRunning();
|
this.running = server.process != null;
|
||||||
|
this.webmine = server.webmine;
|
||||||
this.icon = server.iconFile.isFile();
|
this.icon = server.iconFile.isFile();
|
||||||
|
this.activators = server.getActivators().entrySet().stream().map(ChannelInstanceDto::new).toList();
|
||||||
|
this.observers = server.getObservers().entrySet().stream().map(ChannelInstanceDto::new).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,13 @@
|
|||||||
package de.ph87.mc.server;
|
package de.ph87.mc.server;
|
||||||
|
|
||||||
|
import de.ph87.mc.webmine.Channel;
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.NonNull;
|
import lombok.NonNull;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
|
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||||
|
import org.springframework.scheduling.annotation.Scheduled;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
@ -14,10 +17,12 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
|
@EnableScheduling
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class ServerService {
|
public class ServerService {
|
||||||
|
|
||||||
@ -37,6 +42,21 @@ public class ServerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Scheduled(fixedDelay = 2, timeUnit = TimeUnit.SECONDS)
|
||||||
|
public void schedule() {
|
||||||
|
synchronized (serversLock) {
|
||||||
|
servers.forEach(this::refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refresh(@NonNull final Server server) {
|
||||||
|
if (server.process != null && !server.process.isAlive()) {
|
||||||
|
log.error("Server crash detected: name={}", server.properties.name);
|
||||||
|
server.process = null;
|
||||||
|
publish(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private Optional<Server> _tryLoadingFromDir(@NonNull final File directory) {
|
private Optional<Server> _tryLoadingFromDir(@NonNull final File directory) {
|
||||||
try {
|
try {
|
||||||
@ -61,13 +81,14 @@ public class ServerService {
|
|||||||
|
|
||||||
private void start(@NonNull final Server server) {
|
private void start(@NonNull final Server server) {
|
||||||
synchronized (server.lock) {
|
synchronized (server.lock) {
|
||||||
if (server.isRunning()) {
|
if (server.process != null) {
|
||||||
log.warn("Server is already running: name={}", server.properties.name);
|
log.warn("Server is already running: name={}", server.properties.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
stopAll();
|
stopAll();
|
||||||
|
waitForAllToBeStopped();
|
||||||
log.info("Starting server: name={}", server.properties.name);
|
log.info("Starting server: name={}", server.properties.name);
|
||||||
final ProcessBuilder builder = new ProcessBuilder("java", "-jar", "server.jar");
|
final ProcessBuilder builder = new ProcessBuilder("java", "-Xms2G", "-Xmx2G", "-jar", "server.jar", "nogui");
|
||||||
builder.directory(server.directory);
|
builder.directory(server.directory);
|
||||||
try {
|
try {
|
||||||
server.process = builder.start();
|
server.process = builder.start();
|
||||||
@ -78,9 +99,21 @@ public class ServerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void waitForAllToBeStopped() {
|
||||||
|
synchronized (serversLock) {
|
||||||
|
try {
|
||||||
|
while (servers.stream().anyMatch(server -> server.process != null)) {
|
||||||
|
serversLock.wait(1000);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.error("Interrupted while waiting for ALL servers to stop");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void stop(@NonNull final Server server) {
|
private void stop(@NonNull final Server server) {
|
||||||
synchronized (server.lock) {
|
synchronized (server.lock) {
|
||||||
if (!server.isRunning()) {
|
if (server.process == null) {
|
||||||
log.warn("Server is not running: name={}", server.properties.name);
|
log.warn("Server is not running: name={}", server.properties.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -121,6 +154,14 @@ public class ServerService {
|
|||||||
return publish(server);
|
return publish(server);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@SuppressWarnings("UnusedReturnValue")
|
||||||
|
private <E extends Exception> ServerDto setE(final @NonNull String name, @NonNull final EConsumer<Server, E> modifier) throws E {
|
||||||
|
final Server server = getByName(name);
|
||||||
|
modifier.accept(server);
|
||||||
|
return publish(server);
|
||||||
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
private ServerDto publish(@NonNull final Server server) {
|
private ServerDto publish(@NonNull final Server server) {
|
||||||
final ServerDto dto = new ServerDto(server);
|
final ServerDto dto = new ServerDto(server);
|
||||||
@ -131,7 +172,7 @@ public class ServerService {
|
|||||||
@NonNull
|
@NonNull
|
||||||
private Server getByName(@NonNull final String name) {
|
private Server getByName(@NonNull final String name) {
|
||||||
synchronized (serversLock) {
|
synchronized (serversLock) {
|
||||||
return servers.stream().filter(server -> server.properties.name.equals(name)).findFirst().orElseThrow();
|
return servers.stream().filter(server -> server.properties.name.equals(name)).peek(this::refresh).findFirst().orElseThrow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -156,4 +197,12 @@ public class ServerService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setActivator(final @NonNull String name, final Channel channel, final int signal) throws InvalidChannelSignal {
|
||||||
|
setE(name, server -> server.setActivator(channel, signal));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setObserver(final @NonNull String name, final Channel channel, final int signal) throws InvalidChannelSignal {
|
||||||
|
setE(name, server -> server.setObserver(channel, signal));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
5
src/main/java/de/ph87/mc/webmine/Channel.java
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package de.ph87.mc.webmine;
|
||||||
|
|
||||||
|
public enum Channel {
|
||||||
|
red, blue, green, yellow, magenta, cyan,
|
||||||
|
}
|
||||||
57
src/main/java/de/ph87/mc/webmine/WebmineController.java
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package de.ph87.mc.webmine;
|
||||||
|
|
||||||
|
import de.ph87.mc.server.InvalidChannelSignal;
|
||||||
|
import de.ph87.mc.server.ServerService;
|
||||||
|
import lombok.NonNull;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@CrossOrigin
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("Server")
|
||||||
|
public class WebmineController {
|
||||||
|
|
||||||
|
private final ServerService serverService;
|
||||||
|
|
||||||
|
public WebmineController(final ServerService serverService) {
|
||||||
|
this.serverService = serverService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("Activator/{serverName}/{channelName}/{signal}")
|
||||||
|
public void activator(@NonNull @PathVariable final String serverName, @NonNull @PathVariable String channelName, @PathVariable int signal) {
|
||||||
|
final Channel channel = Arrays.stream(Channel.values()).filter(c -> c.name().equals(channelName)).findFirst().orElseThrow(() -> {
|
||||||
|
final String message = "No such channel: %s".formatted(channelName);
|
||||||
|
log.error(message);
|
||||||
|
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
serverService.setActivator(serverName, channel, signal);
|
||||||
|
} catch (InvalidChannelSignal e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("Observer/{serverName}/{channelName}/{signal}")
|
||||||
|
public void observer(@NonNull @PathVariable final String serverName, @NonNull @PathVariable String channelName, @PathVariable int signal) {
|
||||||
|
final Channel channel = Arrays.stream(Channel.values()).filter(c -> c.name().equals(channelName)).findFirst().orElseThrow(() -> {
|
||||||
|
final String message = "No such channel: %s".formatted(channelName);
|
||||||
|
log.error(message);
|
||||||
|
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
serverService.setObserver(serverName, channel, signal);
|
||||||
|
} catch (InvalidChannelSignal e) {
|
||||||
|
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||