package de.ph87.mc.server; import jakarta.annotation.PostConstruct; 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.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; import java.util.function.Consumer; @Slf4j @Service @RequiredArgsConstructor public class ServerService { private final ApplicationEventPublisher applicationEventPublisher; private final ServerConfig serverConfig; private final Object serversLock = new Object(); private List servers = new ArrayList<>(); @PostConstruct public void startup() { final File ROOT = new File(serverConfig.getPath()); synchronized (serversLock) { servers = Arrays.stream(Objects.requireNonNull(ROOT.listFiles())).map(this::_tryLoadingFromDir).filter(Optional::isPresent).map(Optional::get).toList(); } } @NonNull private Optional _tryLoadingFromDir(@NonNull final File directory) { try { final Server server = new Server(directory); servers.stream().filter(server::eq).findFirst().ifPresent(old -> server.process = old.process); return Optional.of(server); } catch (NoMinecraftServer e) { log.warn(e.getMessage()); } return Optional.empty(); } @NonNull public ServerDto start(@NonNull final String name) { return set(name, this::start); } @NonNull public ServerDto stop(@NonNull final String name) { return set(name, this::stop); } private void start(@NonNull final Server server) { synchronized (server.lock) { if (server.isRunning()) { log.warn("Server is already running: name={}", server.properties.name); return; } stopAll(); log.info("Starting server: name={}", server.properties.name); final ProcessBuilder builder = new ProcessBuilder("java", "-jar", "server.jar"); builder.directory(server.directory); try { server.process = builder.start(); log.info("Server started: name={}", server.properties.name); } catch (IOException e) { log.error("Failed to start Server: name={}, error={}", server.properties.name, e.getMessage()); } } } private void stop(@NonNull final Server server) { synchronized (server.lock) { if (!server.isRunning()) { log.warn("Server is not running: name={}", server.properties.name); return; } if (server.shutdown) { log.warn("Server is already shutting down: name={}", server.properties.name); return; } server.shutdown = true; log.info("Stopping Server: name={}", server.properties.name); new Thread(() -> _stop_async(server), "STOP-" + server.properties.name).start(); } } private void _stop_async(@NonNull final Server server) { log.debug("Thread spawned: name={}", server.properties.name); synchronized (server.lock) { assert server.process != null; log.info("Stopping server: name={}", server.properties.name); server.process.destroy(); try { server.process.waitFor(); log.info("Server stopped: name={}", server.properties.name); } catch (InterruptedException e) { log.error("Interrupted while waiting for server to stop: name={}", server.properties.name); } finally { server.process = null; server.shutdown = false; publish(server); log.debug("Thread terminated: name={}", server.properties.name); } } } @NonNull private ServerDto set(final @NonNull String name, @NonNull final Consumer modifier) { 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); applicationEventPublisher.publishEvent(dto); return dto; } @NonNull private Server getByName(@NonNull final String name) { synchronized (serversLock) { return servers.stream().filter(server -> server.properties.name.equals(name)).findFirst().orElseThrow(); } } @NonNull public List findAll() { synchronized (serversLock) { return servers.stream().map(ServerDto::new).toList(); } } @NonNull public File getIconFileByName(@NonNull final String name) { synchronized (serversLock) { return getByName(name).iconFile; } } @NonNull public List stopAll() { synchronized (serversLock) { return servers.stream().peek(this::stop).map(ServerDto::new).toList(); } } }