125 lines
4.0 KiB
Go
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
|
|
}
|