aboutsummaryrefslogtreecommitdiff
path: root/cmd/thoi
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-09-18 15:16:44 -0400
committerhistoria <[not public]>2026-09-18 15:16:44 -0400
commit7eab582d8a8ee2af1a6baae7baf0696c7005de2e (patch)
tree902bbc8c6e39c9f62f779ca23267714ec420b9f5 /cmd/thoi
parent153acb7f644d755c45533b7c5311e3ffa47e8973 (diff)
downloadthehouseoficarus-7eab582d8a8ee2af1a6baae7baf0696c7005de2e.tar.gz
fix: harden triggers, economy, casino, world data, and admin/net paths
Diffstat (limited to 'cmd/thoi')
-rw-r--r--cmd/thoi/main.go29
1 files changed, 28 insertions, 1 deletions
diff --git a/cmd/thoi/main.go b/cmd/thoi/main.go
index cff6eb6..cf31177 100644
--- a/cmd/thoi/main.go
+++ b/cmd/thoi/main.go
@@ -7,6 +7,7 @@ import (
"os"
"os/signal"
"syscall"
+ "time"
"thehouseoficarus/internal/admin"
"thehouseoficarus/internal/behavior"
@@ -76,6 +77,8 @@ func main() {
adminEvictRoom := g.EvictCasinoRoom
adminReloadCourses := g.CourseStore.ReloadAll
+ // Both admin transports share one UndoStack over the shared history file.
+ var sharedUndo *admin.UndoStack
if cfg.AdminHTTP.Enabled {
adminSrv, err := admin.NewServer(cfg, false, g.AccountStore, g.World, g.ItemStore, g.ObjectStore, g.MobStore, dataDir, adminReloadCourses, adminRewriteRoomIDs, adminEvictRoom, adminRebuildCraftIndex, *configPath, g.RunAdminOnOwner)
if err != nil {
@@ -84,6 +87,7 @@ func main() {
// The raw file editor must refuse a file whose character/account is
// online; wired before ListenAndServe so the first request sees it.
adminSrv.SetPlayerFileOnlineCheck(g.AdminPlayerFileOnline)
+ sharedUndo = adminSrv.UndoStack()
go func() {
if err := adminSrv.ListenAndServe(); err != nil {
log.Printf("admin HTTP server error: %v", err)
@@ -100,6 +104,9 @@ func main() {
// Same online-file refusal as the HTTP editor: both servers expose
// the same raw file API.
adminSrv.SetPlayerFileOnlineCheck(g.AdminPlayerFileOnline)
+ if sharedUndo != nil {
+ adminSrv.SetUndoStack(sharedUndo)
+ }
go func() {
if err := adminSrv.ListenAndServe(); err != nil {
log.Printf("admin HTTPS server error: %v", err)
@@ -133,11 +140,31 @@ func main() {
g.SetTransportStop(server.Shutdown)
go func() {
<-sigCh
+ // Restore default signal handling after the first signal so a second
+ // SIGINT/SIGTERM force-quits a hung shutdown instead of being
+ // swallowed by this one-shot handler.
+ signal.Stop(sigCh)
+ signal.Reset(syscall.SIGINT, syscall.SIGTERM)
g.RequestShutdown("signal")
}()
if err := server.ListenAndServe(g.HandleSessionOnOwner); err != nil {
- log.Fatalf("server error: %v", err)
+ // A transport failure (for example a fatal Accept error) is not a
+ // clean shutdown: start the full shutdown sequence and WAIT for it,
+ // so online state is saved and outstanding wagers are refunded before
+ // exiting non-zero. RequestShutdown is idempotent, so a failure during
+ // a real shutdown just waits on the already-running sequence.
+ log.Printf("server error: %v", err)
+ g.RequestShutdown("transport failure")
+ select {
+ case <-g.ShutdownDone():
+ case <-time.After(30 * time.Second):
+ log.Printf("shutdown: timed out waiting for the shutdown sequence to finish")
+ }
+ if serr := g.ShutdownSaveError(); serr != nil {
+ log.Fatalf("shutdown: final save failed: %v", serr)
+ }
+ os.Exit(1)
}
// The transports close inside the shutdown sequence, so a clean return
// from ListenAndServe is where the final save is known to have finished.