2026-03-14 20:34:37 +01:00
|
|
|
// OwnCord chat server — self-hosted, Windows-native.
|
|
|
|
|
// Build: go build -o chatserver.exe -ldflags "-s -w -X main.version=1.0.0" .
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"os"
|
2026-08-15 20:50:47 +02:00
|
|
|
|
2026-08-27 07:11:18 +02:00
|
|
|
"github.com/J3vb/OwnCord/Server/admin"
|
2026-08-31 05:47:33 +02:00
|
|
|
"github.com/J3vb/OwnCord/Server/internal/app"
|
2026-08-27 07:11:18 +02:00
|
|
|
"github.com/J3vb/OwnCord/Server/logctx"
|
2026-03-14 20:34:37 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// version is overridden at build time via -ldflags "-X main.version=1.0.0".
|
2026-08-31 05:47:33 +02:00
|
|
|
// It stays in package main because that is the symbol the Makefile,
|
|
|
|
|
// release.yml and the Dockerfile name; app.Deps carries it into the process.
|
2026-03-14 20:34:37 +01:00
|
|
|
var version = "dev"
|
|
|
|
|
|
|
|
|
|
func main() {
|
2026-08-15 20:50:47 +02:00
|
|
|
// `server healthcheck` probes the running instance's /health and exits
|
|
|
|
|
// 0/1. It exists for container healthchecks: the distroless image has no
|
|
|
|
|
// shell or curl, so the binary is its own probe.
|
|
|
|
|
if len(os.Args) > 1 && os.Args[1] == "healthcheck" {
|
2026-08-31 05:47:33 +02:00
|
|
|
os.Exit(app.RunHealthcheckCLI())
|
2026-08-15 20:50:47 +02:00
|
|
|
}
|
2026-07-29 13:25:46 +02:00
|
|
|
// `server token ...` is a direct-to-DB CLI (mint/list/revoke API tokens) —
|
|
|
|
|
// handled before any server/logging setup so it stays quiet and standalone.
|
|
|
|
|
if len(os.Args) > 1 && os.Args[1] == "token" {
|
|
|
|
|
os.Exit(runTokenCLI(os.Args[2:]))
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 05:32:40 +01:00
|
|
|
// Create ring buffer for admin log viewer, then build a multi-handler
|
2026-07-31 15:41:57 +02:00
|
|
|
// that tees log records to both stdout and the ring buffer.
|
2026-03-19 05:32:40 +01:00
|
|
|
logBuf := admin.NewRingBuffer(2000)
|
2026-07-31 15:41:57 +02:00
|
|
|
// levelVar controls both handlers' thresholds. It starts at INFO (the
|
2026-08-31 05:47:33 +02:00
|
|
|
// zero value) so early-startup logs are captured, then app.LoadConfig
|
|
|
|
|
// raises or lowers it once config.yaml / OWNCORD_LOGGING_LEVEL is loaded.
|
|
|
|
|
// The ring buffer shares it rather than hard-wiring DEBUG: with both
|
|
|
|
|
// sinks gated, Enabled returns false for suppressed levels and every
|
|
|
|
|
// gated Debug call across the server becomes a no-op instead of
|
|
|
|
|
// formatting a ring entry.
|
2026-07-24 11:06:59 +02:00
|
|
|
levelVar := new(slog.LevelVar)
|
|
|
|
|
stdoutHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: levelVar})
|
2026-07-31 15:41:57 +02:00
|
|
|
multiHandler := admin.NewMultiHandler(stdoutHandler, logBuf, levelVar)
|
2026-07-24 11:06:59 +02:00
|
|
|
// logctx enriches records logged with a request/trace context (the
|
|
|
|
|
// ...Context slog variants) with req_id and, under -tags otel, trace_id.
|
|
|
|
|
log := slog.New(logctx.New(multiHandler))
|
2026-03-19 05:32:40 +01:00
|
|
|
slog.SetDefault(log)
|
2026-03-14 20:34:37 +01:00
|
|
|
|
2026-08-16 08:25:40 +02:00
|
|
|
// The restart coordinator carries a self-restart request (update apply,
|
2026-08-31 05:47:33 +02:00
|
|
|
// backup restore, setup wizard) across the lifecycle's teardown — see
|
|
|
|
|
// internal/app/restart.go. main owns it and hands it in; the handoff
|
|
|
|
|
// below is the last thing this process does. The backstop closure fires
|
|
|
|
|
// only if a requested restart's drain wedges past RestartBackstopDelay:
|
|
|
|
|
// it performs the handoff and force-exits, mirroring what the code below
|
|
|
|
|
// does on the healthy path.
|
|
|
|
|
var rc *app.RestartCoordinator
|
|
|
|
|
rc = app.NewRestartCoordinator(app.RestartBackstopDelay, func() {
|
2026-08-16 08:25:40 +02:00
|
|
|
slog.Error("restart backstop fired — teardown exceeded its budget, exiting for handoff")
|
|
|
|
|
reason, _ := rc.Requested()
|
2026-08-31 05:47:33 +02:00
|
|
|
app.PerformRestartHandoff(reason, rc.Mode(), slog.Default())
|
2026-08-16 08:25:40 +02:00
|
|
|
os.Exit(0)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-31 05:47:33 +02:00
|
|
|
err := runServer(log, logBuf, levelVar, rc)
|
|
|
|
|
rc.Disarm()
|
2026-08-16 08:25:40 +02:00
|
|
|
|
2026-08-31 05:47:33 +02:00
|
|
|
// Perform the handoff even when the lifecycle returned an error: a
|
|
|
|
|
// restart is only ever requested after a committed binary swap or a
|
|
|
|
|
// restore that closed the database, so not restarting is strictly worse
|
|
|
|
|
// than restarting into whatever the error was.
|
2026-08-16 08:25:40 +02:00
|
|
|
if reason, ok := rc.Requested(); ok {
|
2026-08-31 05:47:33 +02:00
|
|
|
app.PerformRestartHandoff(reason, rc.Mode(), log)
|
2026-08-16 08:25:40 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
2026-03-17 04:11:04 +01:00
|
|
|
_, _ = fmt.Fprintf(os.Stderr, "\n [ERROR] %v\n\n", err)
|
2026-03-14 20:34:37 +01:00
|
|
|
log.Error("server exited with error", "error", err)
|
|
|
|
|
os.Exit(1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-31 05:47:33 +02:00
|
|
|
// runServer is the whole server lifecycle: load the configuration main's log
|
|
|
|
|
// sinks and restart coordinator are wired against, build the App around it,
|
|
|
|
|
// and run until it stops and has closed every stage it started. Split out of
|
|
|
|
|
// main() only so the restart handoff above runs on every return path.
|
|
|
|
|
func runServer(log *slog.Logger, logBuf *admin.RingBuffer, levelVar *slog.LevelVar, rc *app.RestartCoordinator) error {
|
|
|
|
|
cfg, err := app.LoadConfig(log, levelVar, rc)
|
2026-08-18 20:39:45 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-31 05:47:33 +02:00
|
|
|
a, err := app.New(cfg, app.Deps{Version: version, Log: log, LogBuf: logBuf, Restart: rc})
|
2026-08-18 20:39:45 +02:00
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2026-08-31 05:47:33 +02:00
|
|
|
return a.Run(context.Background())
|
2026-03-15 07:07:59 +01:00
|
|
|
}
|