Compare commits
2 Commits
f6d5c4ccda
...
0b3c61e1a6
| Author | SHA1 | Date | |
|---|---|---|---|
| 0b3c61e1a6 | |||
| f6406d0dec |
6
src/main/angular/public/error.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50 50">
|
||||
<circle style="fill:#D75A4A;" cx="25" cy="25" r="25"/>
|
||||
<polyline style="fill:none;stroke:#FFFFFF;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;" points="16,34 25,25 34,16"/>
|
||||
<polyline style="fill:none;stroke:#FFFFFF;stroke-width:2;stroke-linecap:round;stroke-miterlimit:10;" points="16,16 25,25 34,34"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 433 B |
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 {ServerListComponent} from './server-list/server-list.component';
|
||||
import {ServerListComponent} from './server/list/server-list.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '**', component: ServerListComponent},
|
||||
|
||||
@ -10,13 +10,21 @@ import {RxStompState} from '@stomp/rx-stomp';
|
||||
})
|
||||
export class ApiService {
|
||||
|
||||
private _connected: boolean = false;
|
||||
|
||||
get connected(): boolean {
|
||||
return this._connected;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly http: HttpClient,
|
||||
readonly stomp: RxStompService,
|
||||
) {
|
||||
this.onConnect(() => this._connected = true);
|
||||
this.onDisconnect(() => this._connected = false);
|
||||
}
|
||||
|
||||
getNone<T>(path: any[], next?: Next<void>): void {
|
||||
getNone(path: any[], next?: Next<void>): void {
|
||||
this.http.get<void>(url('http', path)).subscribe(next);
|
||||
}
|
||||
|
||||
@ -28,7 +36,7 @@ export class ApiService {
|
||||
this.http.get<any[]>(url('http', path)).pipe(map(list => list.map(fromJson))).subscribe(next);
|
||||
}
|
||||
|
||||
postNone<T>(path: any[], data: any, next?: Next<void>): void {
|
||||
postNone(path: any[], data: any, next?: Next<void>): void {
|
||||
this.http.post<void>(url('http', path), data).subscribe(next);
|
||||
}
|
||||
|
||||
|
||||
@ -2,39 +2,61 @@ export type FromJson<T> = (json: any) => T;
|
||||
|
||||
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 {
|
||||
if (t === null) {
|
||||
if (t === null || t === undefined) {
|
||||
return null;
|
||||
}
|
||||
return map(t);
|
||||
}
|
||||
|
||||
export function orElse<T, R>(t: T | null, map: (t: T) => R, orElse: R): R {
|
||||
if (t === null) {
|
||||
export function orElse<T, R>(t: T | null | undefined, map: (t: T) => R, orElse: R): R {
|
||||
if (t === null || t === undefined) {
|
||||
return orElse;
|
||||
}
|
||||
return map(t);
|
||||
}
|
||||
|
||||
export function validateBoolean(json: any) {
|
||||
if (typeof json === 'boolean') {
|
||||
return json;
|
||||
if (typeof json !== 'boolean') {
|
||||
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) {
|
||||
if (typeof json === 'number') {
|
||||
if (typeof json !== 'number') {
|
||||
throw new Error(`Not a Number: type=${typeof json}, value=${JSON.stringify(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 {
|
||||
|
||||
@ -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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #464d55;
|
||||
|
||||
> div {
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
img {
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
@ -25,6 +31,7 @@
|
||||
|
||||
.map {
|
||||
font-weight: bold;
|
||||
|
||||
img {
|
||||
height: 2em;
|
||||
vertical-align: middle;
|
||||
@ -61,6 +68,23 @@
|
||||
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 {NgForOf, NgIf} from '@angular/common';
|
||||
import {Server} from './Server';
|
||||
import {CrudListComponent} from '../crud/CrudListComponent';
|
||||
import {ServerService} from './server.service';
|
||||
import {url} from '../crud/CrudHelpers';
|
||||
import {Channel, Server} from '../Server';
|
||||
import {CrudListComponent} from '../../crud/CrudListComponent';
|
||||
import {ServerService} from '../server.service';
|
||||
import {url} from '../../crud/CrudHelpers';
|
||||
|
||||
@Component({
|
||||
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);
|
||||
}
|
||||
|
||||
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 {CrudService} from '../crud/CrudService';
|
||||
import {Server} from './Server';
|
||||
import {Channel, Server} from './Server';
|
||||
import {ApiService} from '../crud/ApiService';
|
||||
import {Next} from '../crud/CrudHelpers';
|
||||
|
||||
@ -23,4 +23,8 @@ export class ServerService extends CrudService<Server> {
|
||||
this.getSingle([server.name, 'stop'], next);
|
||||
}
|
||||
|
||||
webmineActivatorToggle(server: Server, channel: Channel) {
|
||||
this.getNone(['Activator', server.name, channel, server.isActivatorPowered(channel) === true ? 0 : 15])
|
||||
}
|
||||
|
||||
}
|
||||
@ -12,9 +12,7 @@ body {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.hint {
|
||||
background-color: lightyellow;
|
||||
border: 0.1em solid yellow;
|
||||
.message {
|
||||
margin: 0.5em;
|
||||
padding: 0.25em;
|
||||
font-size: 60%;
|
||||
@ -25,3 +23,17 @@ body {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
.messageInfo {
|
||||
background-color: lightyellow;
|
||||
border: 0.1em solid yellow;
|
||||
}
|
||||
|
||||
.messageError {
|
||||
background-color: #fdaaaa;
|
||||
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();
|
||||
try (final FileReader reader = new FileReader(file)) {
|
||||
properties.load(reader);
|
||||
name = properties.getProperty("level-name");
|
||||
name = directory.getName();
|
||||
motd = properties.getProperty("motd");
|
||||
mode = Mode.valueOf(properties.getProperty("gamemode").toUpperCase(Locale.ROOT));
|
||||
port = Integer.parseInt(properties.getProperty("server-port"));
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
package de.ph87.mc.server;
|
||||
|
||||
import de.ph87.mc.webmine.Channel;
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
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
|
||||
@Slf4j
|
||||
@ -30,6 +36,12 @@ public class Server {
|
||||
|
||||
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 {
|
||||
this.directory = directory;
|
||||
this.pidFile = new File(directory, "pid");
|
||||
@ -37,19 +49,47 @@ public class Server {
|
||||
this.properties = new Properties(directory);
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
synchronized (lock) {
|
||||
return process != null && process.isAlive();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
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) {
|
||||
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.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
public class ServerDto implements IWebsocketMessage {
|
||||
|
||||
@NonNull
|
||||
public final String name;
|
||||
|
||||
@NonNull
|
||||
public final String motd;
|
||||
|
||||
@NonNull
|
||||
public final Mode mode;
|
||||
|
||||
public final int port;
|
||||
|
||||
public final boolean running;
|
||||
|
||||
public final boolean webmine;
|
||||
|
||||
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.motd = server.properties.motd;
|
||||
this.mode = server.properties.mode;
|
||||
this.port = server.properties.port;
|
||||
this.running = server.isRunning();
|
||||
this.running = server.process != null;
|
||||
this.webmine = server.webmine;
|
||||
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;
|
||||
|
||||
import de.ph87.mc.webmine.Channel;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
@ -14,10 +17,12 @@ import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@EnableScheduling
|
||||
@RequiredArgsConstructor
|
||||
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
|
||||
private Optional<Server> _tryLoadingFromDir(@NonNull final File directory) {
|
||||
try {
|
||||
@ -61,13 +81,14 @@ public class ServerService {
|
||||
|
||||
private void start(@NonNull final Server server) {
|
||||
synchronized (server.lock) {
|
||||
if (server.isRunning()) {
|
||||
if (server.process != null) {
|
||||
log.warn("Server is already running: name={}", server.properties.name);
|
||||
return;
|
||||
}
|
||||
stopAll();
|
||||
waitForAllToBeStopped();
|
||||
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);
|
||||
try {
|
||||
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) {
|
||||
synchronized (server.lock) {
|
||||
if (!server.isRunning()) {
|
||||
if (server.process == null) {
|
||||
log.warn("Server is not running: name={}", server.properties.name);
|
||||
return;
|
||||
}
|
||||
@ -121,6 +154,14 @@ public class ServerService {
|
||||
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
|
||||
private ServerDto publish(@NonNull final Server server) {
|
||||
final ServerDto dto = new ServerDto(server);
|
||||
@ -131,7 +172,7 @@ public class ServerService {
|
||||
@NonNull
|
||||
private Server getByName(@NonNull final String name) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||