properties, starting, checking pid/ps, stopping
This commit is contained in:
commit
242d056aeb
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/worlds/
|
||||
1
application.properties
Normal file
1
application.properties
Normal file
@ -0,0 +1 @@
|
||||
server.port=8083
|
||||
49
pom.xml
Normal file
49
pom.xml
Normal file
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>de.ph87</groupId>
|
||||
<artifactId>McManager</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<maven.compiler.release>21</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.4.6</version>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>McManager</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
18
src/main/angular/.editorconfig
Normal file
18
src/main/angular/.editorconfig
Normal file
@ -0,0 +1,18 @@
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.ts]
|
||||
# noinspection EditorConfigKeyCorrectness
|
||||
quote_type = single
|
||||
ij_typescript_use_double_quotes = false
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
42
src/main/angular/.gitignore
vendored
Normal file
42
src/main/angular/.gitignore
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
|
||||
|
||||
# Compiled output
|
||||
/dist
|
||||
/tmp
|
||||
/out-tsc
|
||||
/bazel-out
|
||||
|
||||
# Node
|
||||
/node_modules
|
||||
npm-debug.log
|
||||
yarn-error.log
|
||||
|
||||
# IDEs and editors
|
||||
.idea/
|
||||
.project
|
||||
.classpath
|
||||
.c9/
|
||||
*.launch
|
||||
.settings/
|
||||
*.sublime-workspace
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
!.vscode/tasks.json
|
||||
!.vscode/launch.json
|
||||
!.vscode/extensions.json
|
||||
.history/*
|
||||
|
||||
# Miscellaneous
|
||||
/.angular/cache
|
||||
.sass-cache/
|
||||
/connect.lock
|
||||
/coverage
|
||||
/libpeerconnection.log
|
||||
testem.log
|
||||
/typings
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
59
src/main/angular/README.md
Normal file
59
src/main/angular/README.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Angular
|
||||
|
||||
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 19.2.12.
|
||||
|
||||
## Development server
|
||||
|
||||
To start a local development server, run:
|
||||
|
||||
```bash
|
||||
ng serve
|
||||
```
|
||||
|
||||
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
|
||||
|
||||
## Code scaffolding
|
||||
|
||||
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
|
||||
|
||||
```bash
|
||||
ng generate component component-name
|
||||
```
|
||||
|
||||
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
|
||||
|
||||
```bash
|
||||
ng generate --help
|
||||
```
|
||||
|
||||
## Building
|
||||
|
||||
To build the project run:
|
||||
|
||||
```bash
|
||||
ng build
|
||||
```
|
||||
|
||||
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
|
||||
|
||||
## Running unit tests
|
||||
|
||||
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
|
||||
|
||||
```bash
|
||||
ng test
|
||||
```
|
||||
|
||||
## Running end-to-end tests
|
||||
|
||||
For end-to-end (e2e) testing, run:
|
||||
|
||||
```bash
|
||||
ng e2e
|
||||
```
|
||||
|
||||
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.
|
||||
124
src/main/angular/angular.json
Normal file
124
src/main/angular/angular.json
Normal file
@ -0,0 +1,124 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"angular": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "less",
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:class": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:directive": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:guard": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:interceptor": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:pipe": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:resolver": {
|
||||
"skipTests": true
|
||||
},
|
||||
"@schematics/angular:service": {
|
||||
"skipTests": true
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"sourceRoot": "src",
|
||||
"prefix": "app",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/angular",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
"zone.js"
|
||||
],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"inlineStyleLanguage": "less",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.less"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kB",
|
||||
"maximumError": "1MB"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kB",
|
||||
"maximumError": "8kB"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-devkit/build-angular:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "angular:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "angular:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n"
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "less",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.less"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14882
src/main/angular/package-lock.json
generated
Normal file
14882
src/main/angular/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
38
src/main/angular/package.json
Normal file
38
src/main/angular/package.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "angular",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"build": "ng build && rsync --archive --delete -e 'ssh -p 2222' ./dist/angular/browser/ mc@mc.ph87.de:/srv/McManager/www/",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"test": "ng test"
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@angular/common": "^19.2.0",
|
||||
"@angular/compiler": "^19.2.0",
|
||||
"@angular/core": "^19.2.0",
|
||||
"@angular/forms": "^19.2.0",
|
||||
"@angular/platform-browser": "^19.2.0",
|
||||
"@angular/platform-browser-dynamic": "^19.2.0",
|
||||
"@angular/router": "^19.2.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "~0.15.0",
|
||||
"@stomp/rx-stomp": "^2.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^19.2.12",
|
||||
"@angular/cli": "^19.2.12",
|
||||
"@angular/compiler-cli": "^19.2.0",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"jasmine-core": "~5.6.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"typescript": "~5.7.2"
|
||||
}
|
||||
}
|
||||
BIN
src/main/angular/public/creative.png
Normal file
BIN
src/main/angular/public/creative.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
6
src/main/angular/public/info.svg
Normal file
6
src/main/angular/public/info.svg
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 1024 1024" class="icon" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M512 512m-448 0a448 448 0 1 0 896 0 448 448 0 1 0-896 0Z" fill="#2196F3"/>
|
||||
<path d="M469.333333 469.333333h85.333334v234.666667h-85.333334z" fill="#FFFFFF"/>
|
||||
<path d="M512 352m-53.333333 0a53.333333 53.333333 0 1 0 106.666666 0 53.333333 53.333333 0 1 0-106.666666 0Z" fill="#FFFFFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 425 B |
BIN
src/main/angular/public/survival.png
Normal file
BIN
src/main/angular/public/survival.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
1
src/main/angular/src/app/app.component.html
Normal file
1
src/main/angular/src/app/app.component.html
Normal file
@ -0,0 +1 @@
|
||||
<router-outlet />
|
||||
0
src/main/angular/src/app/app.component.less
Normal file
0
src/main/angular/src/app/app.component.less
Normal file
12
src/main/angular/src/app/app.component.ts
Normal file
12
src/main/angular/src/app/app.component.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
imports: [RouterOutlet],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.less'
|
||||
})
|
||||
export class AppComponent {
|
||||
title = 'angular';
|
||||
}
|
||||
15
src/main/angular/src/app/app.config.ts
Normal file
15
src/main/angular/src/app/app.config.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import {ApplicationConfig, provideZoneChangeDetection} from '@angular/core';
|
||||
import {provideRouter} from '@angular/router';
|
||||
|
||||
import {routes} from './app.routes';
|
||||
import {provideHttpClient} from '@angular/common/http';
|
||||
import {RxStompService, rxStompServiceFactory} from './crud/rx-stomp.service';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideZoneChangeDetection({eventCoalescing: true}),
|
||||
provideRouter(routes),
|
||||
provideHttpClient(),
|
||||
{provide: RxStompService, useFactory: rxStompServiceFactory},
|
||||
]
|
||||
};
|
||||
6
src/main/angular/src/app/app.routes.ts
Normal file
6
src/main/angular/src/app/app.routes.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import {Routes} from '@angular/router';
|
||||
import {ServerListComponent} from './server-list/server-list.component';
|
||||
|
||||
export const routes: Routes = [
|
||||
{path: '**', component: ServerListComponent},
|
||||
];
|
||||
55
src/main/angular/src/app/crud/ApiService.ts
Normal file
55
src/main/angular/src/app/crud/ApiService.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import {HttpClient} from "@angular/common/http";
|
||||
import {FromJson, Next, url} from "./CrudHelpers";
|
||||
import {filter, map, Subscription} from "rxjs";
|
||||
import {Injectable} from '@angular/core';
|
||||
import {RxStompService} from './rx-stomp.service';
|
||||
import {RxStompState} from '@stomp/rx-stomp';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ApiService {
|
||||
|
||||
constructor(
|
||||
readonly http: HttpClient,
|
||||
readonly stomp: RxStompService,
|
||||
) {
|
||||
}
|
||||
|
||||
getNone<T>(path: any[], next?: Next<void>): void {
|
||||
this.http.get<void>(url('http', path)).subscribe(next);
|
||||
}
|
||||
|
||||
getSingle<T>(path: any[], fromJson: FromJson<T>, next?: Next<T>): void {
|
||||
this.http.get<any>(url('http', path)).pipe(map(fromJson)).subscribe(next);
|
||||
}
|
||||
|
||||
getList<T>(path: any[], fromJson: FromJson<T>, next?: Next<T[]>): void {
|
||||
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 {
|
||||
this.http.post<void>(url('http', path), data).subscribe(next);
|
||||
}
|
||||
|
||||
postSingle<T>(path: any[], data: any, fromJson: FromJson<T>, next?: Next<T>): void {
|
||||
this.http.post<any>(url('http', path), data).pipe(map(fromJson)).subscribe(next);
|
||||
}
|
||||
|
||||
postList<T>(path: any[], data: any, fromJson: FromJson<T>, next?: Next<T[]>): void {
|
||||
this.http.post<any[]>(url('http', path), data).pipe(map(list => list.map(fromJson))).subscribe(next);
|
||||
}
|
||||
|
||||
onConnect(next: Next<void>): Subscription {
|
||||
return this.stomp.connectionState$.pipe(filter(state => state === RxStompState.OPEN)).subscribe(() => next());
|
||||
}
|
||||
|
||||
onDisconnect(next: Next<void>): Subscription {
|
||||
return this.stomp.connectionState$.pipe(filter(state => state === RxStompState.CLOSED)).subscribe(() => next());
|
||||
}
|
||||
|
||||
subscribe<T>(path: any[], fromJson: FromJson<T>, next: Next<T>): Subscription {
|
||||
return this.stomp.watch([...path].join('/')).pipe(map(m => fromJson(JSON.parse(m.body)))).subscribe(next);
|
||||
}
|
||||
|
||||
}
|
||||
43
src/main/angular/src/app/crud/CrudHelpers.ts
Normal file
43
src/main/angular/src/app/crud/CrudHelpers.ts
Normal file
@ -0,0 +1,43 @@
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
return map(t);
|
||||
}
|
||||
|
||||
export function orElse<T, R>(t: T | null, map: (t: T) => R, orElse: R): R {
|
||||
if (t === null) {
|
||||
return orElse;
|
||||
}
|
||||
return map(t);
|
||||
}
|
||||
|
||||
export function validateBoolean(json: any) {
|
||||
if (typeof json === 'boolean') {
|
||||
return json;
|
||||
}
|
||||
throw new Error('Not a boolean: ' + JSON.stringify(json));
|
||||
}
|
||||
|
||||
export function validateNumber(json: any) {
|
||||
if (typeof json === 'number') {
|
||||
return json;
|
||||
}
|
||||
throw new Error('Not a number: ' + JSON.stringify(json));
|
||||
}
|
||||
|
||||
export function url(protocol: string, path: any[]): string {
|
||||
const secure = location.protocol === 'https:' ? 's' : '';
|
||||
return `${protocol}${secure}://${location.hostname}:8083/${path.join('/')}`;
|
||||
}
|
||||
38
src/main/angular/src/app/crud/CrudListComponent.ts
Normal file
38
src/main/angular/src/app/crud/CrudListComponent.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import {Injectable, OnDestroy, OnInit} from "@angular/core";
|
||||
import {Subscription} from "rxjs";
|
||||
import {CrudService} from "./CrudService";
|
||||
|
||||
@Injectable()
|
||||
export abstract class CrudListComponent<TYPE, SERVICE extends CrudService<TYPE>> implements OnInit, OnDestroy {
|
||||
|
||||
protected readonly subs: Subscription[] = [];
|
||||
|
||||
protected list: TYPE[] = [];
|
||||
|
||||
protected constructor(
|
||||
readonly crudService: SERVICE,
|
||||
readonly equals: (a: TYPE, b: TYPE) => boolean,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.subs.push(this.crudService.subscribe(item => this.update(item)));
|
||||
this.subs.push(this.crudService.api.onConnect(() => this.crudService.findAll(list => this.list = list)));
|
||||
this.subs.push(this.crudService.api.onDisconnect(() => this.list = []));
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
protected update(item: TYPE) {
|
||||
const index = this.list.findIndex(i => this.equals(i, item));
|
||||
if (index >= 0) {
|
||||
this.list.splice(index, 1, item);
|
||||
} else {
|
||||
this.list.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
48
src/main/angular/src/app/crud/CrudService.ts
Normal file
48
src/main/angular/src/app/crud/CrudService.ts
Normal file
@ -0,0 +1,48 @@
|
||||
import {FromJson, Next} from './CrudHelpers';
|
||||
import {ApiService} from './ApiService';
|
||||
import {Subscription} from 'rxjs';
|
||||
|
||||
export abstract class CrudService<T> {
|
||||
|
||||
protected constructor(
|
||||
readonly api: ApiService,
|
||||
readonly path: any[],
|
||||
readonly fromJson: FromJson<T>,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
findAll(next?: Next<T[]>): void {
|
||||
this.getList(['findAll'], next);
|
||||
}
|
||||
|
||||
protected getNone(path: any[], next?: Next<void>) {
|
||||
this.api.getNone([...this.path, ...path], next);
|
||||
}
|
||||
|
||||
protected getSingle(path: any[], next?: Next<T>) {
|
||||
this.api.getSingle([...this.path, ...path], this.fromJson, next);
|
||||
}
|
||||
|
||||
protected getList(path: any[], next?: Next<T[]>) {
|
||||
this.api.getList([...this.path, ...path], this.fromJson, next);
|
||||
}
|
||||
|
||||
protected postNone(path: any[], data: any, next?: Next<void>) {
|
||||
this.api.postNone([...this.path, ...path], data, next);
|
||||
}
|
||||
|
||||
protected postSingle(path: any[], data: any, next?: Next<T>) {
|
||||
this.api.postSingle([...this.path, ...path], data, this.fromJson, next);
|
||||
}
|
||||
|
||||
protected postList(path: any[], data: any, next?: Next<T[]>) {
|
||||
this.api.postList([...this.path, ...path], data, this.fromJson, next);
|
||||
}
|
||||
|
||||
subscribe(next: Next<T>): Subscription {
|
||||
const subscription = this.api.subscribe([this.path], this.fromJson, order => next(order));
|
||||
return new Subscription(() => subscription.unsubscribe());
|
||||
}
|
||||
|
||||
}
|
||||
28
src/main/angular/src/app/crud/rx-stomp.service.ts
Normal file
28
src/main/angular/src/app/crud/rx-stomp.service.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {RxStomp, RxStompConfig} from "@stomp/rx-stomp";
|
||||
import {url} from './CrudHelpers';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RxStompService extends RxStomp {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const config: RxStompConfig = {
|
||||
brokerURL: url("ws", ["ws"]),
|
||||
heartbeatIncoming: 2000,
|
||||
heartbeatOutgoing: 2000,
|
||||
reconnectDelay: 500,
|
||||
};
|
||||
|
||||
export function rxStompServiceFactory() {
|
||||
const rxStomp = new RxStompService();
|
||||
rxStomp.configure(config);
|
||||
rxStomp.activate();
|
||||
return rxStomp;
|
||||
}
|
||||
5
src/main/angular/src/app/server-list/Mode.ts
Normal file
5
src/main/angular/src/app/server-list/Mode.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export enum Mode {
|
||||
CREATIVE = 'CREATIVE',
|
||||
SURVIVAL = 'SURVIVAL',
|
||||
ADVENTURE = 'ADVENTURE',
|
||||
}
|
||||
40
src/main/angular/src/app/server-list/Server.ts
Normal file
40
src/main/angular/src/app/server-list/Server.ts
Normal file
@ -0,0 +1,40 @@
|
||||
import {Mode} from "./Mode";
|
||||
import {orNull, validateBoolean, validateNumber, validateString} from "../crud/CrudHelpers";
|
||||
|
||||
export class Server {
|
||||
|
||||
constructor(
|
||||
readonly directory: string,
|
||||
readonly name: string,
|
||||
readonly motd: string,
|
||||
readonly mode: Mode,
|
||||
readonly serverPort: number,
|
||||
readonly rconPort: number,
|
||||
readonly rconPassword: string,
|
||||
readonly queryPort: number,
|
||||
readonly pid: number | null,
|
||||
readonly running: boolean,
|
||||
readonly propertyFile: string,
|
||||
readonly pidFile: string,
|
||||
) {
|
||||
//
|
||||
}
|
||||
|
||||
static fromJson(json: any): Server {
|
||||
return new Server(
|
||||
validateString(json.directory),
|
||||
validateString(json.name),
|
||||
validateString(json.motd),
|
||||
validateString(json.mode) as Mode,
|
||||
validateNumber(json.serverPort),
|
||||
validateNumber(json.rconPort),
|
||||
validateString(json.rconPassword),
|
||||
validateNumber(json.queryPort),
|
||||
orNull(json.pid, validateNumber),
|
||||
validateBoolean(json.running),
|
||||
validateString(json.propertyFile),
|
||||
validateString(json.pidFile),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
<div class="heading">
|
||||
Serverliste
|
||||
</div>
|
||||
|
||||
<div class="hint">
|
||||
<img src="info.svg" alt="(i)">
|
||||
Beim Starten eines Servers werden alle anderen gestoppt.
|
||||
</div>
|
||||
|
||||
<div class="list">
|
||||
<div *ngFor="let server of list" class="server">
|
||||
<div class="icon">
|
||||
<img src="{{server.mode.toLowerCase()}}.png" alt="{{server.mode}}">
|
||||
</div>
|
||||
<div class="name">
|
||||
{{ server.motd }}
|
||||
</div>
|
||||
<div class="command start" [class.startInactive]="!server.running" [class.startActive]="server.running" (click)="start(server)">
|
||||
Start
|
||||
</div>
|
||||
<div class="command stop" [class.stopInactive]="server.running" [class.stopActive]="!server.running" (click)="stop(server)">
|
||||
Stopp
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -0,0 +1,52 @@
|
||||
|
||||
.list {
|
||||
|
||||
.server {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
|
||||
> div {
|
||||
padding: 0.25em;
|
||||
}
|
||||
|
||||
.icon {
|
||||
img {
|
||||
height: 2em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
.name {
|
||||
flex-grow: 1;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.command {
|
||||
margin-right: 0.25em;
|
||||
border-radius: 0.25em;
|
||||
}
|
||||
|
||||
.start {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.startActive {
|
||||
background-color: limegreen;
|
||||
box-shadow: 0 0 0.3em 0.1em limegreen, 0 0 0.6em 0.2em rgba(50, 205, 50, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.stop {
|
||||
background-color: indianred;
|
||||
}
|
||||
|
||||
.stopActive {
|
||||
background-color: red;
|
||||
box-shadow: 0 0 0.3em 0.1em red, 0 0 0.6em 0.2em rgba(255, 0, 0, 0.5);
|
||||
color: white;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
import {Component} from '@angular/core';
|
||||
import {NgForOf} from '@angular/common';
|
||||
import {Server} from './Server';
|
||||
import {CrudListComponent} from '../crud/CrudListComponent';
|
||||
import {ServerService} from './server.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-server-list',
|
||||
imports: [
|
||||
NgForOf
|
||||
],
|
||||
templateUrl: './server-list.component.html',
|
||||
styleUrl: './server-list.component.less'
|
||||
})
|
||||
export class ServerListComponent extends CrudListComponent<Server, ServerService> {
|
||||
|
||||
constructor(
|
||||
crudService: ServerService,
|
||||
) {
|
||||
super(crudService, (a, b) => a.name === b.name);
|
||||
}
|
||||
|
||||
start(server: Server) {
|
||||
this.crudService.start(server);
|
||||
}
|
||||
|
||||
stop(server: Server) {
|
||||
this.crudService.stop(server);
|
||||
}
|
||||
|
||||
}
|
||||
26
src/main/angular/src/app/server-list/server.service.ts
Normal file
26
src/main/angular/src/app/server-list/server.service.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {CrudService} from '../crud/CrudService';
|
||||
import {Server} from './Server';
|
||||
import {ApiService} from '../crud/ApiService';
|
||||
import {Next} from '../crud/CrudHelpers';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class ServerService extends CrudService<Server> {
|
||||
|
||||
constructor(
|
||||
api: ApiService,
|
||||
) {
|
||||
super(api, ['Server'], Server.fromJson);
|
||||
}
|
||||
|
||||
start(server: Server, next?: Next<Server>) {
|
||||
this.getSingle([server.name, 'start'], next);
|
||||
}
|
||||
|
||||
stop(server: Server, next?: Next<Server>) {
|
||||
this.getSingle([server.name, 'stop'], next);
|
||||
}
|
||||
|
||||
}
|
||||
13
src/main/angular/src/index.html
Normal file
13
src/main/angular/src/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>McManager</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/png" href="survival.png">
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
6
src/main/angular/src/main.ts
Normal file
6
src/main/angular/src/main.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { AppComponent } from './app/app.component';
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig)
|
||||
.catch((err) => console.error(err));
|
||||
24
src/main/angular/src/styles.less
Normal file
24
src/main/angular/src/styles.less
Normal file
@ -0,0 +1,24 @@
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
font-size: 5vw;
|
||||
}
|
||||
|
||||
.heading {
|
||||
padding: 0.25em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.hint {
|
||||
background-color: lightyellow;
|
||||
border: 0.1em solid yellow;
|
||||
margin: 0.25em;
|
||||
padding: 0.25em;
|
||||
font-size: 60%;
|
||||
border-radius: 0.25em;
|
||||
|
||||
img {
|
||||
height: 1.7em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
15
src/main/angular/tsconfig.app.json
Normal file
15
src/main/angular/tsconfig.app.json
Normal file
@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/app",
|
||||
"types": []
|
||||
},
|
||||
"files": [
|
||||
"src/main.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
27
src/main/angular/tsconfig.json
Normal file
27
src/main/angular/tsconfig.json
Normal file
@ -0,0 +1,27 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist/out-tsc",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"skipLibCheck": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
"moduleResolution": "bundler",
|
||||
"importHelpers": true,
|
||||
"target": "ES2022",
|
||||
"module": "ES2022"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
}
|
||||
}
|
||||
15
src/main/angular/tsconfig.spec.json
Normal file
15
src/main/angular/tsconfig.spec.json
Normal file
@ -0,0 +1,15 @@
|
||||
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
||||
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./out-tsc/spec",
|
||||
"types": [
|
||||
"jasmine"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
13
src/main/java/de/ph87/mc/Backend.java
Normal file
13
src/main/java/de/ph87/mc/Backend.java
Normal file
@ -0,0 +1,13 @@
|
||||
package de.ph87.mc;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Backend {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Backend.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
7
src/main/java/de/ph87/mc/server/Mode.java
Normal file
7
src/main/java/de/ph87/mc/server/Mode.java
Normal file
@ -0,0 +1,7 @@
|
||||
package de.ph87.mc.server;
|
||||
|
||||
public enum Mode {
|
||||
SURVIVAL,
|
||||
CREATIVE,
|
||||
ADVENTURE,
|
||||
}
|
||||
65
src/main/java/de/ph87/mc/server/Server.java
Normal file
65
src/main/java/de/ph87/mc/server/Server.java
Normal file
@ -0,0 +1,65 @@
|
||||
package de.ph87.mc.server;
|
||||
|
||||
import de.ph87.mc.websocket.IWebsocketMessage;
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.Data;
|
||||
import lombok.NonNull;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
@Data
|
||||
public class Server implements IWebsocketMessage {
|
||||
|
||||
@NonNull
|
||||
public final File directory;
|
||||
|
||||
@NonNull
|
||||
public final File propertyFile;
|
||||
|
||||
@NonNull
|
||||
public final File pidFile;
|
||||
|
||||
@NonNull
|
||||
public final String name;
|
||||
|
||||
@NonNull
|
||||
public final String motd;
|
||||
|
||||
@NonNull
|
||||
public final Mode mode;
|
||||
|
||||
public final int serverPort;
|
||||
|
||||
public final int rconPort;
|
||||
|
||||
@NonNull
|
||||
public final String rconPassword;
|
||||
|
||||
public final int queryPort;
|
||||
|
||||
@Nullable
|
||||
private Long pid;
|
||||
|
||||
public Server(@NonNull final File directory, @NonNull final String name, @NonNull final String motd, @NonNull final Mode mode, final int serverPort, final int rconPort, final String rconPassword, final int queryPort) {
|
||||
this.directory = directory;
|
||||
this.propertyFile = new File(directory, "server.properties");
|
||||
this.pidFile = new File(directory, "pid");
|
||||
this.name = name;
|
||||
this.motd = motd;
|
||||
this.mode = mode;
|
||||
this.serverPort = serverPort;
|
||||
this.rconPort = rconPort;
|
||||
this.rconPassword = rconPassword;
|
||||
this.queryPort = queryPort;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Server(%s, \"%s\", %s)".formatted(mode, motd, isRunning() ? "RUNNING" : "stopped");
|
||||
}
|
||||
|
||||
public boolean isRunning() {
|
||||
return pid != null;
|
||||
}
|
||||
|
||||
}
|
||||
14
src/main/java/de/ph87/mc/server/ServerConfig.java
Normal file
14
src/main/java/de/ph87/mc/server/ServerConfig.java
Normal file
@ -0,0 +1,14 @@
|
||||
package de.ph87.mc.server;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "de.ph87.mc.server")
|
||||
public class ServerConfig {
|
||||
|
||||
private String path = "./worlds/";
|
||||
|
||||
}
|
||||
38
src/main/java/de/ph87/mc/server/ServerController.java
Normal file
38
src/main/java/de/ph87/mc/server/ServerController.java
Normal file
@ -0,0 +1,38 @@
|
||||
package de.ph87.mc.server;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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 java.util.List;
|
||||
|
||||
@CrossOrigin
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("Server")
|
||||
public class ServerController {
|
||||
|
||||
private final ServerService serverService;
|
||||
|
||||
private final ServerRepository serverRepository;
|
||||
|
||||
@GetMapping("findAll")
|
||||
public List<Server> findAll() {
|
||||
return serverRepository.findAll();
|
||||
}
|
||||
|
||||
@GetMapping("{name}/start")
|
||||
public Server start(@NonNull @PathVariable final String name) {
|
||||
return serverService.start(name);
|
||||
}
|
||||
|
||||
@GetMapping("{name}/stop")
|
||||
public Server stop(@NonNull @PathVariable final String name) {
|
||||
return serverService.stop(name);
|
||||
}
|
||||
|
||||
}
|
||||
128
src/main/java/de/ph87/mc/server/ServerProcessHelper.java
Normal file
128
src/main/java/de/ph87/mc/server/ServerProcessHelper.java
Normal file
@ -0,0 +1,128 @@
|
||||
package de.ph87.mc.server;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ServerProcessHelper {
|
||||
|
||||
public static final String[] CMDLINE = {"java", "-jar", "server.jar"};
|
||||
|
||||
public static final String CMDLINE_STR = String.join(" ", CMDLINE);
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
public void updatePid(@NonNull final Server server) {
|
||||
server.setPid(_readAndVerifyPid(server));
|
||||
if (server.getPid() == null) {
|
||||
deletePidFile(server);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Long _readAndVerifyPid(@NonNull final Server server) {
|
||||
if (!server.pidFile.exists()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final long pid;
|
||||
try (final FileInputStream stream = new FileInputStream(server.pidFile)) {
|
||||
pid = Long.parseLong(new String(stream.readAllBytes(), StandardCharsets.UTF_8));
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
log.error("Failed to read pid-file: file={}, error={}", server.pidFile, e.getMessage());
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!validateProcFile(server, pid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return pid;
|
||||
}
|
||||
|
||||
private static boolean validateProcFile(@NonNull final Server server, final long pid) {
|
||||
final File procFile = new File("/proc/%d/cmdline".formatted(pid));
|
||||
if (!procFile.exists()) {
|
||||
log.warn("Server not running: {}", server.name);
|
||||
return false;
|
||||
}
|
||||
|
||||
try (final FileInputStream stream = new FileInputStream(procFile)) {
|
||||
final String cmdline = new String(stream.readAllBytes(), StandardCharsets.UTF_8).replace((char) 0, ' ').trim();
|
||||
if (!CMDLINE_STR.equals(cmdline)) {
|
||||
log.error("cmdline of running Server does not match: pid={}, running={}, expected={}", pid, cmdline, CMDLINE_STR);
|
||||
return false;
|
||||
}
|
||||
} catch (IOException | NumberFormatException e) {
|
||||
log.error("Failed to read proc-file: file={}, error={}", procFile, e.getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void deletePidFile(@NonNull final Server server) {
|
||||
server.setPid(null);
|
||||
if (server.pidFile.delete()) {
|
||||
log.info("PID-file removed: {}", server.pidFile);
|
||||
applicationEventPublisher.publishEvent(server);
|
||||
}
|
||||
}
|
||||
|
||||
public void startProcess(@NonNull final Server server) {
|
||||
if (server.isRunning()) {
|
||||
return;
|
||||
}
|
||||
log.info("Starting Server: {}", server.name);
|
||||
final ProcessBuilder builder = new ProcessBuilder(CMDLINE);
|
||||
builder.directory(server.directory);
|
||||
try {
|
||||
final Process process = builder.start();
|
||||
server.setPid(process.pid());
|
||||
writePid(server, process.pid());
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to start server: error={}, name={}", e.getMessage(), server.name);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopProcess(@NonNull final Server server) {
|
||||
if (!server.isRunning()) {
|
||||
return;
|
||||
}
|
||||
new Thread(() -> {
|
||||
try {
|
||||
log.info("Stopping Server: {}", server.name);
|
||||
new ProcessBuilder("kill", "-15", server.getPid() + "").start();
|
||||
while (server.getPid() != null && validateProcFile(server, server.getPid())) {
|
||||
//noinspection BusyWait
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
deletePidFile(server);
|
||||
} catch (IOException | InterruptedException e) {
|
||||
log.error("Failed to stop server: error={}, name={}", e.getMessage(), server.name);
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void writePid(@NonNull final Server server, final long pid) throws IOException {
|
||||
final File file = server.pidFile;
|
||||
try (final FileOutputStream stream = new FileOutputStream(file)) {
|
||||
stream.write("%d".formatted(pid).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
log.info("PID-file written: file={} = {}", server.pidFile, pid);
|
||||
applicationEventPublisher.publishEvent(server);
|
||||
}
|
||||
|
||||
}
|
||||
70
src/main/java/de/ph87/mc/server/ServerRepository.java
Normal file
70
src/main/java/de/ph87/mc/server/ServerRepository.java
Normal file
@ -0,0 +1,70 @@
|
||||
package de.ph87.mc.server;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.server.ResponseStatusException;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Properties;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ServerRepository {
|
||||
|
||||
private final ApplicationEventPublisher applicationEventPublisher;
|
||||
|
||||
private final ServerProcessHelper serverProcessHelper;
|
||||
|
||||
private final ServerConfig serverConfig;
|
||||
|
||||
@NonNull
|
||||
public List<Server> findAll() {
|
||||
final File ROOT = new File(serverConfig.getPath());
|
||||
return Arrays.stream(Objects.requireNonNull(ROOT.listFiles())).map(this::_tryLoadingFromDir).filter(Optional::isPresent).map(Optional::get).toList();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private Optional<Server> _tryLoadingFromDir(@NonNull final File dir) {
|
||||
final File file = new File(dir, "server.properties");
|
||||
if (!file.isFile()) {
|
||||
log.warn("Server directory without server.properties file: {}", dir);
|
||||
return Optional.empty();
|
||||
}
|
||||
final Properties properties = new Properties();
|
||||
try (final FileReader reader = new FileReader(file)) {
|
||||
properties.load(reader);
|
||||
final String name = properties.getProperty("level-name");
|
||||
final String motd = properties.getProperty("motd");
|
||||
final Mode gamemode = Mode.valueOf(properties.getProperty("gamemode").toUpperCase(Locale.ROOT));
|
||||
final int serverPort = Integer.parseInt(properties.getProperty("server-port"));
|
||||
final int rconPort = Integer.parseInt(properties.getProperty("rcon.port"));
|
||||
final String rconPassword = properties.getProperty("rcon.password");
|
||||
final int queryPort = Integer.parseInt(properties.getProperty("query.port"));
|
||||
final Server server = new Server(dir, name, motd, gamemode, serverPort, rconPort, rconPassword, queryPort);
|
||||
serverProcessHelper.updatePid(server);
|
||||
applicationEventPublisher.publishEvent(server);
|
||||
return Optional.of(server);
|
||||
} catch (IOException e) {
|
||||
log.error("Failed to read server.properties: file={}, error={}", file, e.getMessage());
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Server getByName(@NonNull final String name) {
|
||||
return findAll().stream().filter(server -> server.name.equals(name)).findFirst().orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
|
||||
}
|
||||
|
||||
}
|
||||
38
src/main/java/de/ph87/mc/server/ServerService.java
Normal file
38
src/main/java/de/ph87/mc/server/ServerService.java
Normal file
@ -0,0 +1,38 @@
|
||||
package de.ph87.mc.server;
|
||||
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ServerService {
|
||||
|
||||
private final ServerRepository repository;
|
||||
|
||||
private final ServerProcessHelper serverProcessHelper;
|
||||
|
||||
@NonNull
|
||||
public Server start(@NonNull final String name) {
|
||||
final Server server = repository.getByName(name);
|
||||
if (server.isRunning()) {
|
||||
return server;
|
||||
}
|
||||
repository.findAll().forEach(serverProcessHelper::stopProcess);
|
||||
serverProcessHelper.startProcess(server);
|
||||
return server;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Server stop(@NonNull final String name) {
|
||||
final Server server = repository.getByName(name);
|
||||
if (!server.isRunning()) {
|
||||
return server;
|
||||
}
|
||||
serverProcessHelper.stopProcess(server);
|
||||
return server;
|
||||
}
|
||||
|
||||
}
|
||||
13
src/main/java/de/ph87/mc/websocket/IWebsocketMessage.java
Normal file
13
src/main/java/de/ph87/mc/websocket/IWebsocketMessage.java
Normal file
@ -0,0 +1,13 @@
|
||||
package de.ph87.mc.websocket;
|
||||
|
||||
public interface IWebsocketMessage {
|
||||
|
||||
default String getWebsocketDestination() {
|
||||
String name = getClass().getSimpleName();
|
||||
if (name.endsWith("Dto")) {
|
||||
return name.substring(0, name.length() - 4);
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
}
|
||||
40
src/main/java/de/ph87/mc/websocket/WebSocketConfig.java
Normal file
40
src/main/java/de/ph87/mc/websocket/WebSocketConfig.java
Normal file
@ -0,0 +1,40 @@
|
||||
package de.ph87.mc.websocket;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.web.bind.annotation.CrossOrigin;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
@CrossOrigin
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
public static final String DESTINATION = "";
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws").setAllowedOrigins("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
config.enableSimpleBroker(DESTINATION).setHeartbeatValue(new long[]{2000, 2000}).setTaskScheduler(heartBeatScheduler());
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThreadPoolTaskScheduler heartBeatScheduler() {
|
||||
final ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setThreadNamePrefix("wss-heartbeat-");
|
||||
scheduler.setPoolSize(1);
|
||||
scheduler.setRemoveOnCancelPolicy(true);
|
||||
scheduler.setAwaitTerminationSeconds(5);
|
||||
scheduler.setWaitForTasksToCompleteOnShutdown(true);
|
||||
return scheduler;
|
||||
}
|
||||
|
||||
}
|
||||
22
src/main/java/de/ph87/mc/websocket/WebSocketService.java
Normal file
22
src/main/java/de/ph87/mc/websocket/WebSocketService.java
Normal file
@ -0,0 +1,22 @@
|
||||
package de.ph87.mc.websocket;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.messaging.simp.SimpMessageSendingOperations;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketService {
|
||||
|
||||
private final SimpMessageSendingOperations simpMessageSendingOperations;
|
||||
|
||||
@EventListener
|
||||
public void send(@NonNull final IWebsocketMessage message) {
|
||||
simpMessageSendingOperations.convertAndSend(message.getWebsocketDestination(), message);
|
||||
}
|
||||
|
||||
}
|
||||
4
src/main/resources/application.properties
Normal file
4
src/main/resources/application.properties
Normal file
@ -0,0 +1,4 @@
|
||||
logging.level.root=WARN
|
||||
logging.level.de.ph87=INFO
|
||||
#-
|
||||
spring.main.banner-mode=off
|
||||
Loading…
Reference in New Issue
Block a user