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