hachem.wtf
aboutsummaryrefslogtreecommitdiff
path: root/internal/server/server.go
diff options
context:
space:
mode:
authorhachem <im@hachem.wtf>2026-08-29 19:15:28 +0200
committerhachem <im@hachem.wtf>2026-08-29 19:15:28 +0200
commit3eba8fbd243c0ffcb1c396526a4e19d1abc1eaf9 (patch)
tree31613c6da3a3da3c8777d094f5e1466804dbf3a9 /internal/server/server.go
parentfcbbeccaa80c7e120933682b330272a1490c1cf5 (diff)
add: control flow
Diffstat (limited to 'internal/server/server.go')
-rw-r--r--internal/server/server.go196
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
+}