diff options
Diffstat (limited to 'internal/server/server.go')
| -rw-r--r-- | internal/server/server.go | 196 |
1 files changed, 196 insertions, 0 deletions
diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..8e2019d --- /dev/null +++ b/internal/server/server.go @@ -0,0 +1,196 @@ +package server + +import ( + "bufio" + "errors" + "fmt" + "os" + "os/exec" + "path/filepath" + "strconv" + "strings" + "time" + + "github.com/hachem-wtf/benson/internal/config" +) + +const ( + stopTimeout = 60 * time.Second + pingTimeout = 2 * time.Second +) + +type Status struct { + Running bool + PID int + Reachable bool + Version string + MOTD string + Players int + MaxPlayers int + Address string +} + +func pidPath(cfg *config.Config) string { + return filepath.Join(cfg.ServerLocation, "benson-server.pid") +} + +func LogPath(cfg *config.Config) string { + return filepath.Join(cfg.ServerLocation, "benson-server.log") +} + +func javaCmd(cfg *config.Config) string { + if cfg.JavaPath != "" { + return cfg.JavaPath + } + return "java" +} + +func readPID(cfg *config.Config) (int, bool) { + data, err := os.ReadFile(pidPath(cfg)) + if err != nil { + return 0, false + } + pid, err := strconv.Atoi(strings.TrimSpace(string(data))) + if err != nil { + return 0, false + } + return pid, true +} + +func Running(cfg *config.Config) (int, bool) { + pid, ok := readPID(cfg) + if !ok { + return 0, false + } + if !isAlive(pid) { + os.Remove(pidPath(cfg)) + return 0, false + } + return pid, true +} + +func Start(cfg *config.Config) (int, error) { + if cfg.ServerLocation == "" { + return 0, errors.New("server_location must be set in config") + } + if pid, ok := Running(cfg); ok { + return 0, fmt.Errorf("server already running (pid %d)", pid) + } + + jar := filepath.Join(cfg.ServerLocation, "server.jar") + if _, err := os.Stat(jar); err != nil { + return 0, errors.New("no server.jar in server_location — run generate and merge first") + } + + eula := filepath.Join(cfg.ServerLocation, "eula.txt") + if _, err := os.Stat(eula); os.IsNotExist(err) { + if err := os.WriteFile(eula, []byte("eula=true\n"), 0o644); err != nil { + return 0, err + } + } + + logFile, err := os.Create(LogPath(cfg)) + if err != nil { + return 0, err + } + defer logFile.Close() + + cmd := exec.Command(javaCmd(cfg), "-jar", "server.jar", "nogui") + cmd.Dir = cfg.ServerLocation + cmd.Stdout = logFile + cmd.Stderr = logFile + cmd.SysProcAttr = detachAttr() + + if err := cmd.Start(); err != nil { + return 0, err + } + + pid := cmd.Process.Pid + if err := os.WriteFile(pidPath(cfg), []byte(strconv.Itoa(pid)+"\n"), 0o644); err != nil { + return pid, err + } + go cmd.Wait() + return pid, nil +} + +func Stop(cfg *config.Config, progress func(string)) error { + pid, ok := Running(cfg) + if !ok { + return errors.New("server is not running") + } + + progress(fmt.Sprintf("Sending stop signal to pid %d…", pid)) + if err := signalStop(pid); err != nil { + return err + } + + progress("Waiting for the server to save and shut down…") + deadline := time.Now().Add(stopTimeout) + for time.Now().Before(deadline) { + if !isAlive(pid) { + os.Remove(pidPath(cfg)) + return nil + } + time.Sleep(500 * time.Millisecond) + } + + progress("Timed out — forcing shutdown…") + forceKill(pid) + os.Remove(pidPath(cfg)) + return nil +} + +func Restart(cfg *config.Config, progress func(string)) error { + if _, ok := Running(cfg); ok { + if err := Stop(cfg, progress); err != nil { + return err + } + time.Sleep(1 * time.Second) + } + + pid, err := Start(cfg) + if err != nil { + return err + } + progress(fmt.Sprintf("Server started (pid %d).", pid)) + return nil +} + +func GetStatus(cfg *config.Config) Status { + pid, running := Running(cfg) + port := serverPort(cfg) + + s := Status{ + Running: running, + PID: pid, + Address: fmt.Sprintf("localhost:%d", port), + } + + if ping, err := pingServer("localhost", port, pingTimeout); err == nil { + s.Reachable = true + s.Version = ping.Version.Name + s.Players = ping.Players.Online + s.MaxPlayers = ping.Players.Max + s.MOTD = ping.motdText() + } + return s +} + +func serverPort(cfg *config.Config) int { + f, err := os.Open(filepath.Join(cfg.ServerLocation, "server.properties")) + if err != nil { + return 25565 + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if value, ok := strings.CutPrefix(line, "server-port="); ok { + if port, err := strconv.Atoi(strings.TrimSpace(value)); err == nil { + return port + } + } + } + return 25565 +} |
