Files
bd2/go/internal/server/app/serve.go
T

125 lines
4.0 KiB
Go

package app
import (
"bd2server/internal/server/design/gamedata"
"bd2server/internal/server/gateway/auth"
"bd2server/internal/server/gateway/session"
"bd2server/internal/server/gateway/transport"
"bd2server/internal/server/platform/lifecycle"
identitystore "bd2server/internal/server/storage/identity"
"context"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"log/slog"
"net/http"
"os"
"os/signal"
"path/filepath"
"time"
)
func Serve(args []string) (serveErr error) {
defer func() { serveErr = errors.Join(serveErr, gamedata.CloseDatabaseCache()) }()
c, err := loadConfiguration(args)
if err != nil {
return err
}
defer clear(c.authRuntime.MasterKey)
seeds, err := loadSeeds(c)
if err != nil {
return err
}
design, err := loadDesign(c, seeds)
if err != nil {
return err
}
var authService *auth.Service
var authStore *identitystore.Store
if c.authentication.Mode == "oauth" {
authStore, err = identitystore.Open(filepath.Join(c.stateDirectory, "auth.db"), c.authRuntime.MasterKey)
if err != nil {
return fmt.Errorf("open authentication database: %w", err)
}
defer func() { serveErr = errors.Join(serveErr, authStore.Close()) }()
authService, err = auth.New(c.authRuntime, authStore)
if err != nil {
return err
}
}
if err := lockServerState(c.stateDirectory, c.gameRules.Story.StartPackID); err != nil {
return err
}
factory := &PlayerFactory{options: c, design: design, seeds: seeds}
if authStore != nil {
factory.profiles = authStore
}
registry := newPlayerRegistry(factory, 15*time.Minute)
defer func() { serveErr = errors.Join(serveErr, registry.Close(context.Background())) }()
var authenticator session.LoginAuthenticator
if authService != nil {
authenticator = authService
}
game, err := session.NewServer(registry, authenticator)
if err != nil {
return err
}
dispatcher := transport.Bootstrap{Config: c.bootstrap}
var authHandler http.Handler
if authService != nil {
authHandler = authService.Handler()
}
availability := lifecycle.NewGate()
instanceBytes := make([]byte, 16)
if _, err := rand.Read(instanceBytes); err != nil {
return fmt.Errorf("create server instance identity: %w", err)
}
instanceID := hex.EncodeToString(instanceBytes)
handler := transport.HTTP{
Dispatcher: dispatcher, Raw: game, Authentication: c.authentication,
AuthenticationHandler: authHandler, ResourcePolicy: c.publicResources,
CommerceManifest: func() any { return design.cashCatalog.Manifest() }, Availability: availability, InstanceID: instanceID,
}.Handler()
server := &http.Server{
Addr: c.listen,
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 20 * time.Second,
WriteTimeout: 20 * time.Second,
IdleTimeout: 60 * time.Second,
}
slog.Info("BD2 server listening", "address", c.listen, "instance_id", instanceID, "server_version", c.versions.ServerVersion, "game_version", c.bootstrap.Version, "bundle", c.bootstrap.BundleVer, "resourceMode", c.publicResources.Mode, "gameData", c.verifiedGameData.ArchivePath, "gameDataEntries", c.verifiedGameData.EntryCount, "accountSeed", c.accountSeed)
serveResult := make(chan error, 1)
go func() { serveResult <- server.ListenAndServe() }()
signals := make(chan os.Signal, 1)
signal.Notify(signals, lifecycle.ShutdownSignals()...)
defer signal.Stop(signals)
select {
case err := <-serveResult:
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
case received := <-signals:
slog.Info("BD2 server draining", "signal", received.String())
}
availability.Drain()
drainContext, cancelDrain := context.WithTimeout(context.Background(), 60*time.Second)
defer cancelDrain()
if err := availability.Wait(drainContext); err != nil {
slog.Warn("BD2 request drain timed out", "error", err)
}
if err := server.Shutdown(drainContext); err != nil {
_ = server.Close()
return fmt.Errorf("shutdown drained server: %w", err)
}
if err := <-serveResult; err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
slog.Info("BD2 server stopped after drain")
return nil
}