This commit is contained in:
Patrick Haßel 2025-07-31 11:40:44 +02:00
parent f6406d0dec
commit 0b3c61e1a6
56 changed files with 429 additions and 99 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 300 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 299 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 302 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 373 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 343 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 B

View File

@ -1,5 +1,5 @@
import {Routes} from '@angular/router';
import {ServerListComponent} from './server-list/server-list.component';
import {ServerListComponent} from './server/list/server-list.component';
export const routes: Routes = [
{path: '**', component: ServerListComponent},

View File

@ -2,39 +2,61 @@ export type FromJson<T> = (json: any) => T;
export type Next<T> = (t: T) => any;
export function validateString(json: any) {
if (typeof json === 'string') {
return json;
}
throw new Error('Not a string: ' + JSON.stringify(json));
}
export function orNull<T, R>(t: T | null, map: (t: T) => R): R | null {
if (t === null) {
if (t === null || t === undefined) {
return null;
}
return map(t);
}
export function orElse<T, R>(t: T | null, map: (t: T) => R, orElse: R): R {
if (t === null) {
export function orElse<T, R>(t: T | null | undefined, map: (t: T) => R, orElse: R): R {
if (t === null || t === undefined) {
return orElse;
}
return map(t);
}
export function validateBoolean(json: any) {
if (typeof json === 'boolean') {
return json;
if (typeof json !== 'boolean') {
throw new Error(`Not a Boolean: type=${typeof json}, value=${JSON.stringify(json)}`);
}
throw new Error('Not a boolean: ' + JSON.stringify(json));
return json;
}
export function validateNumber(json: any) {
if (typeof json === 'number') {
return json;
if (typeof json !== 'number') {
throw new Error(`Not a Number: type=${typeof json}, value=${JSON.stringify(json)}`);
}
throw new Error('Not a number: ' + JSON.stringify(json));
return json;
}
export function validateString(json: any) {
if (typeof json !== 'string') {
throw new Error(`Not a String: type=${typeof json}, value=${JSON.stringify(json)}`);
}
return json;
}
export function validateList<T>(json: any, fromJson: FromJson<T>): T[] {
if (!Array.isArray(json)) {
throw new Error(`Not a list: type=${typeof json}, value=${JSON.stringify(json)}`);
}
return json.map(fromJson);
}
export function validateMap<K, V>(json: any, key: FromJson<K>, value: FromJson<V>): Map<K, V> {
if (typeof json !== 'object') {
throw new Error(`Not a Map: type=${typeof json}, value=${JSON.stringify(json)}`);
}
const result = new Map<K, V>();
for (const [rawKey, rawValue] of Object.entries(json)) {
const parsedKey = key(rawKey);
const parsedValue = value(rawValue);
result.set(parsedKey, parsedValue);
}
return result;
}
export function url(protocol: string, path: any[]): string {

View File

@ -1,28 +0,0 @@
import {Mode} from "./Mode";
import {validateBoolean, validateNumber, validateString} from "../crud/CrudHelpers";
export class Server {
constructor(
readonly name: string,
readonly motd: string,
readonly mode: Mode,
readonly port: number,
readonly running: boolean,
readonly icon: boolean,
) {
//
}
static fromJson(json: any): Server {
return new Server(
validateString(json.name),
validateString(json.motd),
validateString(json.mode) as Mode,
validateNumber(json.port),
validateBoolean(json.running),
validateBoolean(json.icon),
);
}
}

View File

@ -1,34 +0,0 @@
<div class="heading">
<img src="minecraft.svg" alt="Minecraft">
</div>
<div class="list">
<div *ngFor="let server of servers()" class="server">
<div class="icon">
<img src="{{server.mode.toLowerCase()}}.png" alt="{{server.mode}}" *ngIf="!server.icon">
<img [src]="url('http',['Server', server.name, 'icon'])" alt="{{server.mode}}" *ngIf="server.icon">
</div>
<div class="name">
{{ server.motd }}
<div class="address">
10.255.0.1:{{ server.port }}
</div>
</div>
<div class="map" *ngIf="server.name === 'survival24'">
<a href="https://mc.ph87.de/" target="_blank">
<img src="map.svg" alt="Karte">
</a>
</div>
<div class="command start" [class.startInactive]="!server.running" [class.startActive]="server.running" (click)="start(server)">
&nbsp;An&nbsp;
</div>
<div class="command stop" [class.stopInactive]="server.running" [class.stopActive]="!server.running" (click)="stop(server)">
Aus
</div>
</div>
</div>
<div class="hint">
<img src="info.svg" alt="(i)">
Beim Starten eines Servers werden alle anderen gestoppt.
</div>

View File

@ -0,0 +1,73 @@
import {Mode} from "./Mode";
import {orElse, validateBoolean, validateList, validateNumber, validateString} from "../crud/CrudHelpers";
export enum Channel {
red = 'red',
blue = 'blue',
green = 'green',
yellow = 'yellow',
magenta = 'magenta',
cyan = 'cyan',
}
export function validateChannel(json: any): Channel {
return validateString(json) as Channel;
}
export class ChannelInstance {
constructor(
readonly channel: Channel,
readonly signal: number,
) {
//
}
static fromJson(json: any): ChannelInstance {
return new ChannelInstance(
validateChannel(json.channel),
validateNumber(json.signal),
);
}
}
export class Server {
constructor(
readonly name: string,
readonly motd: string,
readonly mode: Mode,
readonly port: number,
readonly running: boolean,
readonly webmine: boolean,
readonly icon: boolean,
readonly activators: ChannelInstance[],
readonly observers: ChannelInstance[],
) {
//
}
static fromJson(json: any): Server {
return new Server(
validateString(json.name),
validateString(json.motd),
validateString(json.mode) as Mode,
validateNumber(json.port),
validateBoolean(json.running),
validateBoolean(json.webmine),
validateBoolean(json.icon),
validateList(json['activators'], ChannelInstance.fromJson),
validateList(json['observers'], ChannelInstance.fromJson),
);
}
isActivatorPowered(channel: Channel): boolean | null {
return orElse(this.activators.filter(c => c.channel === channel).map(c => c.signal > 0)[0], v => v, null);
}
isObserverPowered(channel: Channel): boolean | null {
return orElse(this.observers.filter(c => c.channel === channel).map(c => c.signal > 0)[0], v => v, null);
}
}

View File

@ -0,0 +1,52 @@
<div class="heading">
<img src="minecraft.svg" alt="Minecraft">
</div>
<div class="list">
<div *ngFor="let server of servers()" class="server">
<div class="icon">
<img src="{{server.mode.toLowerCase()}}.png" alt="{{server.mode}}" *ngIf="!server.icon">
<img [src]="url('http',['Server', server.name, 'icon'])" alt="{{server.mode}}" *ngIf="server.icon">
</div>
<div class="name">
{{ server.motd }}
<div class="address">
10.255.0.1:{{ server.port }}
</div>
</div>
<div class="map" *ngIf="server.name === 'survival24'">
<a href="https://mc.ph87.de/" target="_blank">
<img src="map.svg" alt="Karte">
</a>
</div>
<div class="command start" [class.startInactive]="!server.running" [class.startActive]="server.running" (click)="start(server)">
&nbsp;An&nbsp;
</div>
<div class="command stop" [class.stopInactive]="server.running" [class.stopActive]="!server.running" (click)="stop(server)">
Aus
</div>
<ng-container *ngIf="server.running && server.webmine">
<div class="channelInstanceList">
<div class="channelInstance" *ngFor="let channel of channels()">
<img src="webmine/block/activator/{{channel}}_null.png" [class.hidden]="server.isActivatorPowered(channel) !== null" alt="{{channel}}" (click)="crudService.webmineActivatorToggle(server, channel)">
<img src="webmine/block/activator/{{channel}}_false.png" [class.hidden]="server.isActivatorPowered(channel) !== false" alt="{{channel}}" (click)="crudService.webmineActivatorToggle(server, channel)">
<img src="webmine/block/activator/{{channel}}_true.png" [class.hidden]="server.isActivatorPowered(channel) !== true" alt="{{channel}}" (click)="crudService.webmineActivatorToggle(server, channel)">
</div>
</div>
<div class="channelInstanceList">
<div class="channelInstance" *ngFor="let channel of channels()">
<img src="webmine/block/observer/{{channel}}_null.png" [class.hidden]="server.isObserverPowered(channel) !== null" alt="{{channel}}">
<img src="webmine/block/observer/{{channel}}_false.png" [class.hidden]="server.isObserverPowered(channel) !== false" alt="{{channel}}">
<img src="webmine/block/observer/{{channel}}_true.png" [class.hidden]="server.isObserverPowered(channel) !== true" alt="{{channel}}">
</div>
</div>
</ng-container>
</div>
</div>
<div class="message messageError" *ngIf="!crudService.api.connected">
<img src="error.svg" alt="(x)">
Nicht verbunden!
</div>

View File

@ -3,16 +3,22 @@
.server {
display: flex;
flex-wrap: wrap;
flex-direction: row;
align-items: center;
border-bottom: 1px solid #464d55;
> div {
padding: 0.25em;
}
.icon {
img {
height: 2em;
width: 2em;
text-align: center;
img {
height: 100%;
vertical-align: middle;
}
}
@ -25,6 +31,7 @@
.map {
font-weight: bold;
img {
height: 2em;
vertical-align: middle;
@ -61,6 +68,23 @@
color: white;
}
.channelInstanceList {
width: 100%;
display: flex;
flex-direction: row;
.channelInstance {
display: block;
width: calc(100% / 6);
padding-left: 0.25em;
padding-right: 0.25em;
img {
width: 100%;
}
}
}
}
}

View File

@ -1,9 +1,9 @@
import {Component} from '@angular/core';
import {NgForOf, NgIf} from '@angular/common';
import {Server} from './Server';
import {CrudListComponent} from '../crud/CrudListComponent';
import {ServerService} from './server.service';
import {url} from '../crud/CrudHelpers';
import {Channel, Server} from '../Server';
import {CrudListComponent} from '../../crud/CrudListComponent';
import {ServerService} from '../server.service';
import {url} from '../../crud/CrudHelpers';
@Component({
selector: 'app-server-list',
@ -36,4 +36,8 @@ export class ServerListComponent extends CrudListComponent<Server, ServerService
return this.list.sort((a, b) => a.port - b.port);
}
channels(): Channel[] {
return Object.keys(Channel).filter(key => isNaN(Number(key))).map(key => key as Channel);
}
}

View File

@ -1,6 +1,6 @@
import {Injectable} from '@angular/core';
import {CrudService} from '../crud/CrudService';
import {Server} from './Server';
import {Channel, Server} from './Server';
import {ApiService} from '../crud/ApiService';
import {Next} from '../crud/CrudHelpers';
@ -23,4 +23,8 @@ export class ServerService extends CrudService<Server> {
this.getSingle([server.name, 'stop'], next);
}
webmineActivatorToggle(server: Server, channel: Channel) {
this.getNone(['Activator', server.name, channel, server.isActivatorPowered(channel) === true ? 0 : 15])
}
}

View File

@ -33,3 +33,7 @@ body {
background-color: #fdaaaa;
border: 0.1em solid red;
}
.hidden {
display: none;
}

View File

@ -0,0 +1,22 @@
package de.ph87.mc.server;
import de.ph87.mc.webmine.Channel;
import lombok.Data;
import lombok.NonNull;
import java.util.Map;
@Data
public class ChannelInstanceDto {
@NonNull
public final Channel channel;
public final int signal;
public ChannelInstanceDto(@NonNull final Map.Entry<Channel, Integer> entry) {
this.channel = entry.getKey();
this.signal = entry.getValue();
}
}

View File

@ -0,0 +1,8 @@
package de.ph87.mc.server;
@FunctionalInterface
public interface EConsumer<T, E extends Exception> {
void accept(final T t) throws E;
}

View File

@ -0,0 +1,12 @@
package de.ph87.mc.server;
import de.ph87.mc.webmine.Channel;
import lombok.NonNull;
public class InvalidChannelSignal extends Exception {
public InvalidChannelSignal(@NonNull final Server server, @NonNull final Channel channel, final int signal) {
super("Invalid signal received: server=%s, channel=%s, signal=%s".formatted(server.properties.name, channel, signal));
}
}

View File

@ -31,7 +31,7 @@ public class Properties {
final java.util.Properties properties = new java.util.Properties();
try (final FileReader reader = new FileReader(file)) {
properties.load(reader);
name = properties.getProperty("level-name");
name = directory.getName();
motd = properties.getProperty("motd");
mode = Mode.valueOf(properties.getProperty("gamemode").toUpperCase(Locale.ROOT));
port = Integer.parseInt(properties.getProperty("server-port"));

View File

@ -1,11 +1,17 @@
package de.ph87.mc.server;
import de.ph87.mc.webmine.Channel;
import jakarta.annotation.Nullable;
import lombok.Data;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import java.io.File;
import java.io.IOException;
import java.net.URI;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
@Data
@Slf4j
@ -30,6 +36,12 @@ public class Server {
public boolean shutdown = false;
public boolean webmine = false;
private final Map<Channel, Integer> activators = new HashMap<>();
private final Map<Channel, Integer> observers = new HashMap<>();
public Server(@NonNull final File directory) throws NoMinecraftServer {
this.directory = directory;
this.pidFile = new File(directory, "pid");
@ -37,19 +49,47 @@ public class Server {
this.properties = new Properties(directory);
}
public boolean isRunning() {
synchronized (lock) {
return process != null && process.isAlive();
}
}
@Override
public String toString() {
return "Server(%s, \"%s\", %s)".formatted(properties.mode, properties.motd, isRunning() ? "RUNNING" : "stopped");
return "Server(%s, \"%s\", %s)".formatted(properties.mode, properties.motd, process != null ? "RUNNING" : "stopped");
}
public boolean eq(@NonNull final Server other) {
return properties.name.equals(other.properties.name);
}
public void setActivator(final Channel channel, final int signal) throws InvalidChannelSignal {
if (signal < 0 || signal > 15) {
throw new InvalidChannelSignal(this, channel, signal);
}
synchronized (activators) {
activators.put(channel, signal);
publish(channel, signal);
log.info("Activator changed: name={}, channel={}, signal={}", properties.name, channel, signal);
}
}
public void setObserver(final Channel channel, final int signal) throws InvalidChannelSignal {
if (signal < 0 || signal > 15) {
throw new InvalidChannelSignal(this, channel, signal);
}
synchronized (observers) {
observers.put(channel, signal);
log.info("Observer changed: name={}: {}={}", properties.name, channel, signal);
}
}
private void publish(@NonNull final Channel channel, final int signal) {
new Thread(() -> {
try {
final URLConnection con = URI.create("http://localhost:8123/set?channel=%s&signal=%d".formatted(channel, signal)).toURL().openConnection();
con.setConnectTimeout(500);
con.setReadTimeout(500);
con.connect();
} catch (IOException e) {
log.error("Failed to publish Activator to Server: name={}, channel={}, signal={}, error={}", properties.name, channel, signal, e.getMessage());
}
}).start();
}
}

View File

@ -4,28 +4,44 @@ import de.ph87.mc.websocket.IWebsocketMessage;
import lombok.Data;
import lombok.NonNull;
import java.util.List;
@Data
public class ServerDto implements IWebsocketMessage {
@NonNull
public final String name;
@NonNull
public final String motd;
@NonNull
public final Mode mode;
public final int port;
public final boolean running;
public final boolean webmine;
public boolean icon;
public ServerDto(final @NonNull Server server) {
@NonNull
public final List<ChannelInstanceDto> activators;
@NonNull
public final List<ChannelInstanceDto> observers;
public ServerDto(@NonNull final Server server) {
this.name = server.properties.name;
this.motd = server.properties.motd;
this.mode = server.properties.mode;
this.port = server.properties.port;
this.running = server.isRunning();
this.running = server.process != null;
this.webmine = server.webmine;
this.icon = server.iconFile.isFile();
this.activators = server.getActivators().entrySet().stream().map(ChannelInstanceDto::new).toList();
this.observers = server.getObservers().entrySet().stream().map(ChannelInstanceDto::new).toList();
}
}

View File

@ -1,10 +1,13 @@
package de.ph87.mc.server;
import de.ph87.mc.webmine.Channel;
import jakarta.annotation.PostConstruct;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.io.File;
@ -14,10 +17,12 @@ import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
@Slf4j
@Service
@EnableScheduling
@RequiredArgsConstructor
public class ServerService {
@ -37,6 +42,21 @@ public class ServerService {
}
}
@Scheduled(fixedDelay = 2, timeUnit = TimeUnit.SECONDS)
public void schedule() {
synchronized (serversLock) {
servers.forEach(this::refresh);
}
}
private void refresh(@NonNull final Server server) {
if (server.process != null && !server.process.isAlive()) {
log.error("Server crash detected: name={}", server.properties.name);
server.process = null;
publish(server);
}
}
@NonNull
private Optional<Server> _tryLoadingFromDir(@NonNull final File directory) {
try {
@ -61,13 +81,14 @@ public class ServerService {
private void start(@NonNull final Server server) {
synchronized (server.lock) {
if (server.isRunning()) {
if (server.process != null) {
log.warn("Server is already running: name={}", server.properties.name);
return;
}
stopAll();
waitForAllToBeStopped();
log.info("Starting server: name={}", server.properties.name);
final ProcessBuilder builder = new ProcessBuilder("java", "-jar", "server.jar");
final ProcessBuilder builder = new ProcessBuilder("java", "-Xms2G", "-Xmx2G", "-jar", "server.jar", "nogui");
builder.directory(server.directory);
try {
server.process = builder.start();
@ -78,9 +99,21 @@ public class ServerService {
}
}
private void waitForAllToBeStopped() {
synchronized (serversLock) {
try {
while (servers.stream().anyMatch(server -> server.process != null)) {
serversLock.wait(1000);
}
} catch (InterruptedException e) {
log.error("Interrupted while waiting for ALL servers to stop");
}
}
}
private void stop(@NonNull final Server server) {
synchronized (server.lock) {
if (!server.isRunning()) {
if (server.process == null) {
log.warn("Server is not running: name={}", server.properties.name);
return;
}
@ -121,6 +154,14 @@ public class ServerService {
return publish(server);
}
@NonNull
@SuppressWarnings("UnusedReturnValue")
private <E extends Exception> ServerDto setE(final @NonNull String name, @NonNull final EConsumer<Server, E> modifier) throws E {
final Server server = getByName(name);
modifier.accept(server);
return publish(server);
}
@NonNull
private ServerDto publish(@NonNull final Server server) {
final ServerDto dto = new ServerDto(server);
@ -131,7 +172,7 @@ public class ServerService {
@NonNull
private Server getByName(@NonNull final String name) {
synchronized (serversLock) {
return servers.stream().filter(server -> server.properties.name.equals(name)).findFirst().orElseThrow();
return servers.stream().filter(server -> server.properties.name.equals(name)).peek(this::refresh).findFirst().orElseThrow();
}
}
@ -156,4 +197,12 @@ public class ServerService {
}
}
public void setActivator(final @NonNull String name, final Channel channel, final int signal) throws InvalidChannelSignal {
setE(name, server -> server.setActivator(channel, signal));
}
public void setObserver(final @NonNull String name, final Channel channel, final int signal) throws InvalidChannelSignal {
setE(name, server -> server.setObserver(channel, signal));
}
}

View File

@ -0,0 +1,5 @@
package de.ph87.mc.webmine;
public enum Channel {
red, blue, green, yellow, magenta, cyan,
}

View File

@ -0,0 +1,57 @@
package de.ph87.mc.webmine;
import de.ph87.mc.server.InvalidChannelSignal;
import de.ph87.mc.server.ServerService;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ResponseStatusException;
import java.util.Arrays;
@Slf4j
@CrossOrigin
@RestController
@RequestMapping("Server")
public class WebmineController {
private final ServerService serverService;
public WebmineController(final ServerService serverService) {
this.serverService = serverService;
}
@GetMapping("Activator/{serverName}/{channelName}/{signal}")
public void activator(@NonNull @PathVariable final String serverName, @NonNull @PathVariable String channelName, @PathVariable int signal) {
final Channel channel = Arrays.stream(Channel.values()).filter(c -> c.name().equals(channelName)).findFirst().orElseThrow(() -> {
final String message = "No such channel: %s".formatted(channelName);
log.error(message);
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
});
try {
serverService.setActivator(serverName, channel, signal);
} catch (InvalidChannelSignal e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
}
}
@GetMapping("Observer/{serverName}/{channelName}/{signal}")
public void observer(@NonNull @PathVariable final String serverName, @NonNull @PathVariable String channelName, @PathVariable int signal) {
final Channel channel = Arrays.stream(Channel.values()).filter(c -> c.name().equals(channelName)).findFirst().orElseThrow(() -> {
final String message = "No such channel: %s".formatted(channelName);
log.error(message);
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
});
try {
serverService.setObserver(serverName, channel, signal);
} catch (InvalidChannelSignal e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
}
}
}