Add admin API for content reload

This commit is contained in:
Ilya Groshev
2026-04-28 21:22:28 +03:00
parent 9be0df4c30
commit 3fe564cb1d
36 changed files with 992 additions and 638 deletions
+22 -5
View File
@@ -93,6 +93,10 @@ func main() {
grpcOctoURL := flag.String("grpc.octo-url", "", "Octo CDN base URL passed to lunar-tear (default: derived from cdn.public-addr)")
grpcAuthURL := flag.String("grpc.auth-url", "", "auth server base URL passed to lunar-tear (default: derived from auth.listen)")
// admin webhook is opt-in; empty leaves lunar-tear's own default in place
// (the listener still only binds if LUNAR_ADMIN_TOKEN is set in the env).
adminListen := flag.String("admin.listen", "", "lunar-tear admin webhook listen address (host:port). Empty = leave default; webhook only binds when LUNAR_ADMIN_TOKEN is set in the env.")
noColor := flag.Bool("no-color", false, "disable colored output")
flag.Parse()
@@ -139,11 +143,7 @@ func main() {
label: "grpc",
color: colorYellow,
cmd: exec.CommandContext(ctx, filepath.Join("bin", "lunar-tear"+ext),
"--listen", *grpcListen,
"--public-addr", *grpcPublicAddr,
"--db", *grpcDB,
"--octo-url", *grpcOctoURL,
"--auth-url", *grpcAuthURL,
grpcArgs(*grpcListen, *grpcPublicAddr, *grpcDB, *grpcOctoURL, *grpcAuthURL, *adminListen)...,
),
},
}
@@ -200,3 +200,20 @@ func prefixLines(wg *sync.WaitGroup, prefix string, r io.Reader) {
fmt.Printf("%s%s\n", prefix, scanner.Text())
}
}
// grpcArgs assembles the argv for the lunar-tear subprocess. The admin flag
// is appended only when --admin.listen was supplied so we don't override
// lunar-tear's own default when the operator hasn't opted in.
func grpcArgs(listen, publicAddr, db, octoURL, authURL, adminListen string) []string {
args := []string{
"--listen", listen,
"--public-addr", publicAddr,
"--db", db,
"--octo-url", octoURL,
"--auth-url", authURL,
}
if adminListen != "" {
args = append(args, "--admin-listen", adminListen)
}
return args
}
+56
View File
@@ -0,0 +1,56 @@
package main
import (
"crypto/subtle"
"log"
"net/http"
"os"
"lunar-tear/server/internal/runtime"
)
// startAdmin spins up the admin webhook used by external content tools to
// trigger an in-place re-read of assets/release/20240404193219.bin.e.
//
// Authentication: Bearer token via the LUNAR_ADMIN_TOKEN environment variable.
// If LUNAR_ADMIN_TOKEN is unset or empty the listener does not bind at all
// (fail closed), so a fresh deploy never exposes an unauthenticated endpoint.
//
// The default --admin-listen is 127.0.0.1:8082 so the webhook is only
// reachable via loopback unless the operator opts in by binding to 0.0.0.0.
func startAdmin(listen string, holder *runtime.Holder) {
token := os.Getenv("LUNAR_ADMIN_TOKEN")
if token == "" {
log.Println("[admin] disabled (no LUNAR_ADMIN_TOKEN set)")
return
}
expected := []byte("Bearer " + token)
mux := http.NewServeMux()
mux.HandleFunc("/api/admin/master-data/reload", func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
got := []byte(r.Header.Get("Authorization"))
if len(got) != len(expected) || subtle.ConstantTimeCompare(got, expected) != 1 {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
if err := holder.Reload(); err != nil {
log.Printf("[admin] master-data reload failed: %v", err)
http.Error(w, "master-data reload failed", http.StatusInternalServerError)
return
}
log.Printf("[admin] master-data reloaded by %s", r.RemoteAddr)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"ok":true}`))
})
log.Printf("[admin] webhook listener on %s (token-gated)", listen)
go func() {
if err := http.ListenAndServe(listen, mux); err != nil {
log.Printf("[admin] webhook listener failed: %v", err)
}
}()
}
+26 -94
View File
@@ -6,10 +6,8 @@ import (
"strconv"
pb "lunar-tear/server/gen/proto"
"lunar-tear/server/internal/gacha"
"lunar-tear/server/internal/interceptor"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/service"
"lunar-tear/server/internal/store"
@@ -40,27 +38,7 @@ func startGRPC(
store.UserRepository
store.SessionRepository
},
questEngine *questflow.QuestHandler,
gachaHandler *gacha.GachaHandler,
gachaEntries []store.GachaCatalogEntry,
cageOrnamentCatalog *masterdata.CageOrnamentCatalog,
loginBonusCatalog *masterdata.LoginBonusCatalog,
characterViewerCatalog *masterdata.CharacterViewerCatalog,
shopCatalog *masterdata.ShopCatalog,
costumeCatalog *masterdata.CostumeCatalog,
omikujiCatalog *masterdata.OmikujiCatalog,
weaponCatalog *masterdata.WeaponCatalog,
exploreCatalog *masterdata.ExploreCatalog,
gimmickCatalog *masterdata.GimmickCatalog,
characterBoardCatalog *masterdata.CharacterBoardCatalog,
partsCatalog *masterdata.PartsCatalog,
characterRebirthCatalog *masterdata.CharacterRebirthCatalog,
companionCatalog *masterdata.CompanionCatalog,
materialCatalog *masterdata.MaterialCatalog,
consumableItemCatalog *masterdata.ConsumableItemCatalog,
gameConfig *masterdata.GameConfig,
sideStoryCatalog *masterdata.SideStoryCatalog,
bigHuntCatalog *masterdata.BigHuntCatalog,
holder *runtime.Holder,
) *grpc.Server {
lis, err := net.Listen("tcp", listenAddr)
if err != nil {
@@ -74,33 +52,7 @@ func startGRPC(
grpc.UnknownServiceHandler(interceptor.UnknownService),
)
registerServices(grpcServer,
publicAddr,
octoURL,
authURL,
userStore,
questEngine,
gachaHandler,
gachaEntries,
cageOrnamentCatalog,
loginBonusCatalog,
characterViewerCatalog,
shopCatalog,
costumeCatalog,
omikujiCatalog,
weaponCatalog,
exploreCatalog,
gimmickCatalog,
characterBoardCatalog,
partsCatalog,
characterRebirthCatalog,
companionCatalog,
materialCatalog,
consumableItemCatalog,
gameConfig,
sideStoryCatalog,
bigHuntCatalog,
)
registerServices(grpcServer, publicAddr, octoURL, authURL, userStore, holder)
reflection.Register(grpcServer)
@@ -124,66 +76,46 @@ func registerServices(
store.UserRepository
store.SessionRepository
},
questEngine *questflow.QuestHandler,
gachaHandler *gacha.GachaHandler,
gachaEntries []store.GachaCatalogEntry,
cageOrnamentCatalog *masterdata.CageOrnamentCatalog,
loginBonusCatalog *masterdata.LoginBonusCatalog,
characterViewerCatalog *masterdata.CharacterViewerCatalog,
shopCatalog *masterdata.ShopCatalog,
costumeCatalog *masterdata.CostumeCatalog,
omikujiCatalog *masterdata.OmikujiCatalog,
weaponCatalog *masterdata.WeaponCatalog,
exploreCatalog *masterdata.ExploreCatalog,
gimmickCatalog *masterdata.GimmickCatalog,
characterBoardCatalog *masterdata.CharacterBoardCatalog,
partsCatalog *masterdata.PartsCatalog,
characterRebirthCatalog *masterdata.CharacterRebirthCatalog,
companionCatalog *masterdata.CompanionCatalog,
materialCatalog *masterdata.MaterialCatalog,
consumableItemCatalog *masterdata.ConsumableItemCatalog,
gameConfig *masterdata.GameConfig,
sideStoryCatalog *masterdata.SideStoryCatalog,
bigHuntCatalog *masterdata.BigHuntCatalog,
holder *runtime.Holder,
) {
pubHost, pubPortStr, _ := net.SplitHostPort(publicAddr)
pubPort, _ := strconv.Atoi(pubPortStr)
pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(gachaEntries))
pb.RegisterBannerServiceServer(srv, service.NewBannerServiceServer(holder))
pb.RegisterUserServiceServer(srv, service.NewUserServiceServer(userStore, userStore, authURL))
pb.RegisterBattleServiceServer(srv, service.NewBattleServiceServer(userStore, userStore))
pb.RegisterConfigServiceServer(srv, service.NewConfigServiceServer(pubHost, int32(pubPort), octoURL))
pb.RegisterDataServiceServer(srv, service.NewDataServiceServer(userStore, userStore))
pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, questEngine))
pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, gachaEntries, gachaHandler))
pb.RegisterTutorialServiceServer(srv, service.NewTutorialServiceServer(userStore, userStore, holder))
pb.RegisterGachaServiceServer(srv, service.NewGachaServiceServer(userStore, userStore, holder))
pb.RegisterGiftServiceServer(srv, service.NewGiftServiceServer(userStore, userStore))
pb.RegisterGamePlayServiceServer(srv, service.NewGameplayServiceServer())
pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, gimmickCatalog))
pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, questEngine))
pb.RegisterGimmickServiceServer(srv, service.NewGimmickServiceServer(userStore, userStore, holder))
pb.RegisterQuestServiceServer(srv, service.NewQuestServiceServer(userStore, userStore, holder))
pb.RegisterNotificationServiceServer(srv, service.NewNotificationServiceServer(userStore, userStore))
pb.RegisterCageOrnamentServiceServer(srv, service.NewCageOrnamentServiceServer(userStore, userStore, cageOrnamentCatalog, questEngine.Granter))
pb.RegisterCageOrnamentServiceServer(srv, service.NewCageOrnamentServiceServer(userStore, userStore, holder))
pb.RegisterDeckServiceServer(srv, service.NewDeckServiceServer(userStore, userStore))
pb.RegisterFriendServiceServer(srv, service.NewFriendServiceServer(userStore, userStore))
pb.RegisterLoginBonusServiceServer(srv, service.NewLoginBonusServiceServer(userStore, userStore, loginBonusCatalog))
pb.RegisterLoginBonusServiceServer(srv, service.NewLoginBonusServiceServer(userStore, userStore, holder))
pb.RegisterNaviCutInServiceServer(srv, service.NewNaviCutInServiceServer(userStore, userStore))
pb.RegisterContentsStoryServiceServer(srv, service.NewContentsStoryServiceServer(userStore, userStore))
pb.RegisterDokanServiceServer(srv, service.NewDokanServiceServer(userStore, userStore))
pb.RegisterPortalCageServiceServer(srv, service.NewPortalCageServiceServer(userStore, userStore))
pb.RegisterCharacterViewerServiceServer(srv, service.NewCharacterViewerServiceServer(userStore, userStore, characterViewerCatalog))
pb.RegisterCharacterViewerServiceServer(srv, service.NewCharacterViewerServiceServer(userStore, userStore, holder))
pb.RegisterMissionServiceServer(srv, service.NewMissionServiceServer(userStore, userStore))
pb.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, shopCatalog, questEngine.Granter))
pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, costumeCatalog, gameConfig))
pb.RegisterShopServiceServer(srv, service.NewShopServiceServer(userStore, userStore, holder))
pb.RegisterCostumeServiceServer(srv, service.NewCostumeServiceServer(userStore, userStore, holder))
pb.RegisterMovieServiceServer(srv, service.NewMovieServiceServer(userStore, userStore))
pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, omikujiCatalog))
pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, weaponCatalog, gameConfig))
pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, exploreCatalog))
pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, characterBoardCatalog))
pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, partsCatalog, gameConfig))
pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, characterRebirthCatalog, gameConfig))
pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, companionCatalog, gameConfig))
pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, materialCatalog, gameConfig))
pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, consumableItemCatalog, gameConfig))
pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, sideStoryCatalog))
pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, bigHuntCatalog, questEngine))
pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, bigHuntCatalog, questEngine.Granter))
pb.RegisterOmikujiServiceServer(srv, service.NewOmikujiServiceServer(userStore, userStore, holder))
pb.RegisterWeaponServiceServer(srv, service.NewWeaponServiceServer(userStore, userStore, holder))
pb.RegisterExploreServiceServer(srv, service.NewExploreServiceServer(userStore, userStore, holder))
pb.RegisterCharacterBoardServiceServer(srv, service.NewCharacterBoardServiceServer(userStore, userStore, holder))
pb.RegisterPartsServiceServer(srv, service.NewPartsServiceServer(userStore, userStore, holder))
pb.RegisterCharacterServiceServer(srv, service.NewCharacterServiceServer(userStore, userStore, holder))
pb.RegisterCompanionServiceServer(srv, service.NewCompanionServiceServer(userStore, userStore, holder))
pb.RegisterMaterialServiceServer(srv, service.NewMaterialServiceServer(userStore, userStore, holder))
pb.RegisterConsumableItemServiceServer(srv, service.NewConsumableItemServiceServer(userStore, userStore, holder))
pb.RegisterSideStoryQuestServiceServer(srv, service.NewSideStoryQuestServiceServer(userStore, userStore, holder))
pb.RegisterBigHuntServiceServer(srv, service.NewBigHuntServiceServer(userStore, userStore, holder))
pb.RegisterRewardServiceServer(srv, service.NewRewardServiceServer(userStore, userStore, holder))
}
+9 -156
View File
@@ -9,30 +9,30 @@ import (
"syscall"
"lunar-tear/server/internal/database"
"lunar-tear/server/internal/gacha"
"lunar-tear/server/internal/gametime"
"lunar-tear/server/internal/masterdata"
"lunar-tear/server/internal/masterdata/memorydb"
"lunar-tear/server/internal/questflow"
"lunar-tear/server/internal/runtime"
"lunar-tear/server/internal/store/sqlite"
)
const masterDataPath = "assets/release/20240404193219.bin.e"
func main() {
listen := flag.String("listen", "0.0.0.0:443", "gRPC listen address (host:port)")
publicAddr := flag.String("public-addr", "127.0.0.1:443", "externally-reachable host:port advertised to clients")
dbPath := flag.String("db", "db/game.db", "SQLite database path")
octoURL := flag.String("octo-url", "", "Octo CDN base URL the client will use for assets (e.g. http://10.0.2.2:8080)")
authURL := flag.String("auth-url", "", "Auth server base URL for Facebook token validation (e.g. http://localhost:3000)")
adminListen := flag.String("admin-listen", "127.0.0.1:8082", "admin webhook listen address (host:port). Loopback by default; only binds when LUNAR_ADMIN_TOKEN is set.")
flag.Parse()
if *octoURL == "" {
log.Fatalf("--octo-url is required (e.g. http://10.0.2.2:8080)")
}
if err := memorydb.Init("assets/release/20240404193219.bin.e"); err != nil {
log.Fatalf("load master data: %v", err)
holder, err := runtime.NewHolder(masterDataPath)
if err != nil {
log.Fatalf("init master data: %v", err)
}
log.Printf("master data loaded (%d tables)", memorydb.TableCount())
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
@@ -44,158 +44,11 @@ func main() {
defer db.Close()
log.Printf("database opened: %s", *dbPath)
gameConfig, err := masterdata.LoadGameConfig()
if err != nil {
log.Fatalf("load game config: %v", err)
}
log.Printf("game config loaded (goldId=%d, skipTicketId=%d, rebirthGold=%d)",
gameConfig.ConsumableItemIdForGold, gameConfig.ConsumableItemIdForQuestSkipTicket, gameConfig.CharacterRebirthConsumeGold)
partsCatalog, err := masterdata.LoadPartsCatalog()
if err != nil {
log.Fatalf("load parts catalog: %v", err)
}
log.Printf("parts catalog loaded: %d parts, %d rarities", len(partsCatalog.PartsById), len(partsCatalog.RarityByRarityType))
questCatalog, err := masterdata.LoadQuestCatalog(partsCatalog)
if err != nil {
log.Fatalf("load quest catalog: %v", err)
}
questHandler := questflow.NewQuestHandler(questCatalog, gameConfig)
userStore := sqlite.New(db, gametime.Now)
gachaEntries, medalInfo, err := masterdata.LoadGachaCatalog()
if err != nil {
log.Fatalf("load gacha catalog: %v", err)
}
log.Printf("gacha catalog loaded: %d entries", len(gachaEntries))
grpcServer := startGRPC(*listen, *publicAddr, *octoURL, *authURL, userStore, holder)
gachaPool, err := masterdata.LoadGachaPool()
if err != nil {
log.Fatalf("load gacha pool: %v", err)
}
log.Printf("gacha pool loaded: costumes=%d rarities, weapons=%d rarities, materials=%d",
len(gachaPool.CostumesByRarity), len(gachaPool.WeaponsByRarity), len(gachaPool.Materials))
shopCatalog, err := masterdata.LoadShopCatalog()
if err != nil {
log.Fatalf("load shop catalog: %v", err)
}
log.Printf("shop catalog loaded: %d items, %d content groups, %d exchange shops",
len(shopCatalog.Items), len(shopCatalog.Contents), len(shopCatalog.ExchangeShopCells))
gachaPool.BuildShopFeatured(shopCatalog)
gachaPool.PruneUnpairedCostumes()
gachaPool.BuildFeaturedMapping(gachaEntries)
gachaPool.BuildBannerPools(gachaEntries)
masterdata.EnrichCatalogPromotions(gachaEntries, gachaPool)
dupExchange, err := masterdata.LoadDupExchange()
if err != nil {
log.Fatalf("load dup exchange: %v", err)
}
dupAdded, err := masterdata.EnrichDupExchange(dupExchange, gachaPool)
if err != nil {
log.Fatalf("enrich dup exchange: %v", err)
}
log.Printf("dup exchange loaded: %d entries (%d derived from limit-break materials)", len(dupExchange), dupAdded)
gachaHandler := gacha.NewGachaHandler(gachaPool, gameConfig, questHandler.Granter, medalInfo, dupExchange)
conditionResolver, err := masterdata.LoadConditionResolver()
if err != nil {
log.Fatalf("load condition resolver: %v", err)
}
cageOrnamentCatalog := masterdata.LoadCageOrnamentCatalog()
loginBonusCatalog := masterdata.LoadLoginBonusCatalog()
characterViewerCatalog := masterdata.LoadCharacterViewerCatalog(conditionResolver)
omikujiCatalog := masterdata.LoadOmikujiCatalog()
materialCatalog, err := masterdata.LoadMaterialCatalog()
if err != nil {
log.Fatalf("load material catalog: %v", err)
}
log.Printf("material catalog loaded: %d materials", len(materialCatalog.All))
consumableItemCatalog, err := masterdata.LoadConsumableItemCatalog()
if err != nil {
log.Fatalf("load consumable item catalog: %v", err)
}
log.Printf("consumable item catalog loaded: %d items", len(consumableItemCatalog.All))
costumeCatalog, err := masterdata.LoadCostumeCatalog(materialCatalog)
if err != nil {
log.Fatalf("load costume catalog: %v", err)
}
log.Printf("costume catalog loaded: %d costumes, %d materials, %d rarity curves", len(costumeCatalog.Costumes), len(costumeCatalog.Materials), len(costumeCatalog.ExpByRarity))
weaponCatalog, err := masterdata.LoadWeaponCatalog(materialCatalog)
if err != nil {
log.Fatalf("load weapon catalog: %v", err)
}
log.Printf("weapon catalog loaded: %d weapons, %d materials, %d enhance configs", len(weaponCatalog.Weapons), len(weaponCatalog.Materials), len(weaponCatalog.ExpByEnhanceId))
exploreCatalog, err := masterdata.LoadExploreCatalog()
if err != nil {
log.Fatalf("load explore catalog: %v", err)
}
log.Printf("explore catalog loaded: %d explores, %d grade assets", len(exploreCatalog.Explores), len(exploreCatalog.GradeAssets))
gimmickCatalog, err := masterdata.LoadGimmickCatalog(conditionResolver)
if err != nil {
log.Fatalf("load gimmick catalog: %v", err)
}
characterBoardCatalog, err := masterdata.LoadCharacterBoardCatalog()
if err != nil {
log.Fatalf("load character board catalog: %v", err)
}
log.Printf("character board catalog loaded: %d panels, %d boards", len(characterBoardCatalog.PanelById), len(characterBoardCatalog.BoardById))
characterRebirthCatalog, err := masterdata.LoadCharacterRebirthCatalog()
if err != nil {
log.Fatalf("load character rebirth catalog: %v", err)
}
log.Printf("character rebirth catalog loaded: %d characters", len(characterRebirthCatalog.StepGroupByCharacterId))
companionCatalog, err := masterdata.LoadCompanionCatalog()
if err != nil {
log.Fatalf("load companion catalog: %v", err)
}
log.Printf("companion catalog loaded: %d companions, %d categories", len(companionCatalog.CompanionById), len(companionCatalog.GoldCostByCategory))
sideStoryCatalog := masterdata.LoadSideStoryCatalog()
bigHuntCatalog := masterdata.LoadBigHuntCatalog()
grpcServer := startGRPC(
*listen,
*publicAddr,
*octoURL,
*authURL,
userStore,
questHandler,
gachaHandler,
gachaEntries,
cageOrnamentCatalog,
loginBonusCatalog,
characterViewerCatalog,
shopCatalog,
costumeCatalog,
omikujiCatalog,
weaponCatalog,
exploreCatalog,
gimmickCatalog,
characterBoardCatalog,
partsCatalog,
characterRebirthCatalog,
companionCatalog,
materialCatalog,
consumableItemCatalog,
gameConfig,
sideStoryCatalog,
bigHuntCatalog,
)
startAdmin(*adminListen, holder)
<-ctx.Done()
log.Println("shutting down...")
+58 -17
View File
@@ -32,13 +32,14 @@ const (
)
type config struct {
IP string `json:"ip"`
Device string `json:"device"`
Detail string `json:"detail"`
Summary string `json:"summary"`
GRPCPort int `json:"grpc_port,omitempty"`
CDNPort int `json:"cdn_port,omitempty"`
AuthPort int `json:"auth_port,omitempty"`
IP string `json:"ip"`
Device string `json:"device"`
Detail string `json:"detail"`
Summary string `json:"summary"`
GRPCPort int `json:"grpc_port,omitempty"`
CDNPort int `json:"cdn_port,omitempty"`
AuthPort int `json:"auth_port,omitempty"`
AdminPort int `json:"admin_port,omitempty"`
}
const (
@@ -47,10 +48,13 @@ const (
defaultAuthPort = 3000
)
// ports.Admin is opt-in: 0 means the admin webhook is not configured by the
// wizard at all. Other ports always get a default if unset.
type ports struct {
GRPC int
CDN int
Auth int
GRPC int
CDN int
Auth int
Admin int
}
func main() {
@@ -59,6 +63,7 @@ func main() {
grpcPort := flag.Int("grpc-port", defaultGRPCPort, "gRPC server port")
cdnPort := flag.Int("cdn-port", defaultCDNPort, "CDN server port")
authPort := flag.Int("auth-port", defaultAuthPort, "auth server port")
adminPort := flag.Int("admin-port", 0, "admin webhook port (0 = disabled). Bound on 127.0.0.1; only takes effect when LUNAR_ADMIN_TOKEN is set.")
flag.Parse()
flagSet := map[string]bool{}
@@ -80,10 +85,10 @@ func main() {
ip, cfg, firstRun := resolveIP(*preferSaved)
p := resolvePorts(flagSet, *grpcPort, *cdnPort, *authPort, cfg)
p := resolvePorts(flagSet, *grpcPort, *cdnPort, *authPort, *adminPort, cfg)
savedPorts := portsFromConfig(cfg)
if !firstRun && (p.GRPC != savedPorts.GRPC || p.CDN != savedPorts.CDN || p.Auth != savedPorts.Auth) {
if !firstRun && (p.GRPC != savedPorts.GRPC || p.CDN != savedPorts.CDN || p.Auth != savedPorts.Auth || p.Admin != savedPorts.Admin) {
if !warnPortChange(savedPorts, p) {
os.Exit(0)
}
@@ -92,6 +97,7 @@ func main() {
cfg.GRPCPort = p.GRPC
cfg.CDNPort = p.CDN
cfg.AuthPort = p.Auth
cfg.AdminPort = p.Admin
saveConfig(cfg)
labelStyle := lipgloss.NewStyle().Foreground(lipgloss.Color("8")).Width(14)
@@ -101,6 +107,9 @@ func main() {
fmt.Printf(" %s %s\n", labelStyle.Render("Game server:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.GRPC)))
fmt.Printf(" %s %s\n", labelStyle.Render("CDN:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.CDN)))
fmt.Printf(" %s %s\n", labelStyle.Render("Auth:"), addrStyle.Render(fmt.Sprintf("%s:%d", ip, p.Auth)))
if p.Admin > 0 {
fmt.Printf(" %s %s\n", labelStyle.Render("Admin webhook:"), addrStyle.Render(fmt.Sprintf("127.0.0.1:%d", p.Admin)))
}
fmt.Println()
if firstRun || *setupOnly {
@@ -477,6 +486,22 @@ func warnPortChange(old, new ports) bool {
}
return hlStyle.Render(fmt.Sprintf(" %-7s %d → %d", label+":", oldP, newP))
}
// Admin formatting handles the disabled (0) state since the port is
// opt-in and we don't want to display "0" to the user.
adminLine := func(oldP, newP int) (string, bool) {
switch {
case oldP == 0 && newP == 0:
return "", false
case oldP == 0 && newP != 0:
return hlStyle.Render(fmt.Sprintf(" %-7s disabled → %d", "Admin:", newP)), true
case oldP != 0 && newP == 0:
return hlStyle.Render(fmt.Sprintf(" %-7s %d → disabled", "Admin:", oldP)), true
case oldP == newP:
return dimStyle.Render(fmt.Sprintf(" %-7s %d (unchanged)", "Admin:", oldP)), true
default:
return hlStyle.Render(fmt.Sprintf(" %-7s %d → %d", "Admin:", oldP, newP)), true
}
}
var b strings.Builder
b.WriteString("\n")
@@ -487,7 +512,12 @@ func warnPortChange(old, new ports) bool {
b.WriteString(portLine("CDN", old.CDN, new.CDN))
b.WriteString("\n")
b.WriteString(portLine("Auth", old.Auth, new.Auth))
b.WriteString("\n\n")
b.WriteString("\n")
if line, show := adminLine(old.Admin, new.Admin); show {
b.WriteString(line)
b.WriteString("\n")
}
b.WriteString("\n")
b.WriteString(dimStyle.Render(" Your APK was patched for the old ports. You may need to re-patch."))
b.WriteString("\n\n")
fmt.Print(b.String())
@@ -821,7 +851,7 @@ func loadConfig() (config, error) {
}
func portsFromConfig(cfg config) ports {
p := ports{GRPC: cfg.GRPCPort, CDN: cfg.CDNPort, Auth: cfg.AuthPort}
p := ports{GRPC: cfg.GRPCPort, CDN: cfg.CDNPort, Auth: cfg.AuthPort, Admin: cfg.AdminPort}
if p.GRPC == 0 {
p.GRPC = defaultGRPCPort
}
@@ -831,10 +861,11 @@ func portsFromConfig(cfg config) ports {
if p.Auth == 0 {
p.Auth = defaultAuthPort
}
// Admin is opt-in: leave 0 = disabled.
return p
}
func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag int, saved config) ports {
func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag, adminFlag int, saved config) ports {
resolve := func(name string, flagVal, savedVal, defaultVal int) int {
if flagSet[name] {
return flagVal
@@ -848,6 +879,9 @@ func resolvePorts(flagSet map[string]bool, grpcFlag, cdnFlag, authFlag int, save
GRPC: resolve("grpc-port", grpcFlag, saved.GRPCPort, defaultGRPCPort),
CDN: resolve("cdn-port", cdnFlag, saved.CDNPort, defaultCDNPort),
Auth: resolve("auth-port", authFlag, saved.AuthPort, defaultAuthPort),
// defaultVal=0 keeps admin opt-in: never enabled unless --admin-port
// is passed or a non-zero value was previously saved.
Admin: resolve("admin-port", adminFlag, saved.AdminPort, 0),
}
}
@@ -874,13 +908,20 @@ func launchDev(ip string, p ports) {
runQuiet(exec.Command("go", "build", "-o", devBin, "./cmd/dev"), "build dev")
}).Run()
cmd := exec.Command(devBin,
devArgs := []string{
"--grpc.listen", fmt.Sprintf("0.0.0.0:%d", p.GRPC),
"--grpc.public-addr", fmt.Sprintf("%s:%d", ip, p.GRPC),
"--cdn.listen", fmt.Sprintf("0.0.0.0:%d", p.CDN),
"--cdn.public-addr", fmt.Sprintf("%s:%d", ip, p.CDN),
"--auth.listen", fmt.Sprintf("0.0.0.0:%d", p.Auth),
)
}
// Bind admin on loopback only — the wizard is for local dev, and the
// webhook should never be exposed to the LAN by accident. Operators who
// want a different bind can run cmd/dev directly with --admin.listen.
if p.Admin > 0 {
devArgs = append(devArgs, "--admin.listen", fmt.Sprintf("127.0.0.1:%d", p.Admin))
}
cmd := exec.Command(devBin, devArgs...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Stdin = os.Stdin