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); } }